File Coverage

blib/lib/Auto/Mata.pm
Criterion Covered Total %
statement 138 139 99.2
branch 37 38 97.3
condition 6 8 75.0
subroutine 26 27 96.3
pod 9 13 69.2
total 216 225 96.0


line stmt bran cond sub pod time code
1             package Auto::Mata;
2             # ABSTRACT: A simple, reliable finite state machine
3             $Auto::Mata::VERSION = '0.07';
4              
5 1     1   245060 use strict;
  1         2  
  1         30  
6 1     1   4 use warnings;
  1         2  
  1         31  
7 1     1   448 use parent 'Exporter';
  1         263  
  1         5  
8 1     1   45 use Carp;
  1         1  
  1         79  
9 1     1   552 use Data::Dumper;
  1         5422  
  1         70  
10 1     1   7 use List::Util qw(first reduce);
  1         1  
  1         58  
11 1     1   615 use Storable qw(dclone);
  1         2404  
  1         58  
12 1     1   6 use Type::Utils -all;
  1         1  
  1         15  
13 1     1   1727 use Types::Standard -all;
  1         1  
  1         10  
14 1     1   24628 use Type::Params qw(compile);
  1         9101  
  1         8  
15              
16             our @EXPORT = qw(
17             machine
18             ready
19             terminal
20             term
21             transition
22             to
23             on
24             with
25             using
26             );
27              
28             our $DEBUG = $ENV{DEBUG_AUTOMATA};
29              
30             my $Ident = declare 'Ident', as StrMatch[qr/^[A-Z][_0-9A-Z]*$/i];
31             my $State = declare 'State', as Tuple[$Ident, Any];
32             my $Type = declare 'Type', as InstanceOf['Type::Tiny'];
33             coerce $Type, from Undef, via { Any };
34              
35             my $Transition = declare 'Transition', as Dict[
36             to => $Ident,
37             initial => $Type,
38             transform => Maybe[CodeRef],
39             ];
40              
41             my $Automata = declare 'Automata', as Dict[
42             ready => Maybe[$Ident],
43             term => Maybe[$Ident],
44             map => Map[$Ident, ArrayRef[$Transition]],
45             ];
46              
47              
48             sub machine (&) {
49 12     12 1 7451 my $code = shift;
50              
51             #-----------------------------------------------------------------------------
52             # Define the machine parameters
53             #-----------------------------------------------------------------------------
54 12         35 my %fsm = (ready => undef, term => undef, map => {});
55              
56 12         14 do {
57 12         15 local $_ = \%fsm;
58 12         17 $code->();
59 11         706 validate();
60             };
61              
62 2         3 my %map = %{$fsm{map}};
  2         7  
63 2         3 my $ready = $fsm{ready};
64 2         2 my $term = $fsm{term};
65 2         7 my $Terminal = declare 'Terminal', as Tuple[Enum[$term], Any];
66              
67 2         3806 my %final = ($term => $Terminal);
68              
69 2         8 foreach my $from (keys %map) {
70 5         319 my @next = map { $_->{initial} } @{$map{$from}};
  7         17  
  5         12  
71 5     2   47 $final{$from} = declare $from, as reduce { $a | $b } @next;
  2         154  
72             }
73              
74             #-----------------------------------------------------------------------------
75             # Build the transition engine
76             #-----------------------------------------------------------------------------
77 2         135 my @match;
78 2         6 foreach my $from (keys %map) {
79             #---------------------------------------------------------------------------
80             # Create a type constraint that matches each possible initial "from" state.
81             # Use this to build a matching function that calls the appropriate mutator
82             # for that transisiton.
83             #---------------------------------------------------------------------------
84 5         5 foreach my $transition (@{$map{$from}}) {
  5         9  
85 7         8 my $to = $transition->{to};
86 7         4 my $initial = $transition->{initial};
87 7         9 my $with = $transition->{transform};
88 7         5 my $final = $final{$to};
89              
90             push @match, $initial, sub {
91 11     11   1038 my ($from, $input) = @$_;
92 11         25 debug('%s -> %s: %s', $from, $to, explain($input));
93              
94 11 100       24 do { local $_ = $input; $input = $with->() } if $with;
  8         8  
  8         17  
95 11         43 my $state = [$to, $input];
96              
97 11 100       28 if (defined(my $error = $final->validate($state))) {
98 1 50       114 if (my $explain = $final->validate_explain($state, 'FINAL_STATE')) {
99 1         1071 debug($_) foreach @$explain;
100             }
101              
102 1         5 croak join "\n",
103             sprintf('Transition from %s to %s produced an invalid state.', $from, $to),
104             sprintf('Attempted to move from %s to %s', explain($_), explain($state)),
105             sprintf($error);
106             }
107              
108 10         5457 return @$state;
109 7         44 };
110             }
111             }
112              
113 2     1   7 my $default = sub { croak 'no transitions match ' . explain($_) };
  1         152  
114 2         9 my $transform = compile_match_on_type(@match, => $default);
115              
116             #-----------------------------------------------------------------------------
117             # Return function that builds a transition engine for the given input
118             #-----------------------------------------------------------------------------
119             return sub {
120 4     4   3106 my $interactive = shift;
121              
122 4         7 my $state = $ready;
123 4         3 my $done;
124              
125             my $iter = sub (\$) {
126 14 100       1331 return if $done;
127 12         238 ($state, $_[0]) = $transform->([$state, $_[0]]);
128 10         28 $done = $Terminal->check([$state, $_[0]]);
129 10 100       133 wantarray ? ($state, $_[0]) : $state;
130 4         14 };
131              
132 4 100       14 return $iter if $interactive;
133              
134             return sub (\$) {
135 2         5 while (my ($state, $acc) = $iter->($_[0])) {
136             ;
137             }
138              
139 1         5 return $_[0];
140 2         11 };
141 2         2051 };
142             }
143              
144              
145 12     12 1 238 sub ready ($) { assert_in_the_machine(); $_->{ready} = shift }
  10         330  
146 10     10 1 218 sub terminal ($) { assert_in_the_machine(); $_->{term} = shift }
  9         245  
147 1     1 1 9 sub term ($) { goto \&terminal }
148 16     16 1 48 sub to ($;%) { (to => shift, @_) }
149 15     15 1 3369 sub on ($;%) { (on => shift, @_) }
150 3     3 1 20 sub with (&;%) { (with => shift, @_) }
151 0     0 1 0 sub using (&;%) { goto \&with }
152              
153              
154             my $_transition_args;
155              
156             sub transition ($%) {
157 17     17 1 1776 assert_in_the_machine();
158 16   66     1009 $_transition_args ||= compile($Ident, $Ident, $Type, Maybe[CodeRef]);
159              
160 16         2183 my ($arg, %param) = @_;
161 16         48 my ($from, $to, $on, $with) = $_transition_args->($arg, @param{qw(to on with)});
162              
163 16   100     551 $_->{map}{$from} ||= [];
164              
165 16         35 my $name = $on->name;
166 16         96 my $init = declare "${from}_to_${to}_on_${name}", as Tuple[Enum[$from], $on];
167 16         31790 debug("New state: $init");
168              
169 16         24 foreach my $next (@{$_->{map}{$from}}) {
  16         45  
170             croak "identical transition $next->{initial} already defined"
171 5 100       387 if $init == $next->{initial};
172             }
173              
174 15         459 my $transition = {
175             to => $to,
176             initial => $init,
177             transform => $with,
178             };
179              
180             # Add this contraint to the list of matches
181 15         14 push @{$_->{map}{$from}}, $transition;
  15         52  
182             }
183              
184             #-------------------------------------------------------------------------------
185             # Throws an error when not within a call to `machine`. When debugging, includes
186             # the full `validate_explain` if the error was due to a type-checking failure.
187             #-------------------------------------------------------------------------------
188             sub assert_in_the_machine {
189 50 100   50 0 349 croak 'cannot be called outside a state machine definition block' unless $_;
190              
191 47 100       102 unless (!defined(my $msg = $Automata->validate_explain($_, '$_'))) {
192 2         1242 debug('Invalid machine state detected: %s', join("\n", map {" -$_"} @$msg));
  9         26  
193 2         263 croak 'invalid machine definition';
194             }
195             }
196              
197             #-------------------------------------------------------------------------------
198             # Emits a debug message preceded by 'DEBUG> ' to STDERR when $DEBUG is true.
199             # Behaves like `warn(sprintf(@_))` in all other respects.
200             #-------------------------------------------------------------------------------
201             sub debug {
202 38 100   38 0 1300 return unless $DEBUG;
203 1         3 my ($msg, @args) = @_;
204 1         14 warn sprintf("# DEBUG> $msg\n", @args);
205             }
206              
207             #-------------------------------------------------------------------------------
208             # Alias for Data::Dumper::Dumper with no Indent and Terse output.
209             #-------------------------------------------------------------------------------
210             sub explain {
211 15     15 0 4441 my $state = shift;
212 15         21 local $Data::Dumper::Indent = 0;
213 15         13 local $Data::Dumper::Terse = 1;
214 15         39 Dumper($state);
215             }
216              
217             #-------------------------------------------------------------------------------
218             # Validate sanity as much as possible without strict types and without
219             # guarantees on the return type of transitions.
220             #-------------------------------------------------------------------------------
221             sub validate {
222 11     11 0 14 assert_in_the_machine();
223              
224             croak 'no ready state defined'
225 10 100       772 unless $_->{ready};
226              
227             croak 'no terminal state defined'
228 9 100       99 unless $_->{term};
229              
230             croak 'terminal state and ready state are identical'
231 8 100       92 if $_->{ready} eq $_->{term};
232              
233             croak 'no transitions defined'
234 7 100       9 unless keys %{$_->{map}};
  7         93  
235              
236             croak 'no transition defined for ready state'
237             unless $_->{map}{$_->{ready}}
238 6 100 66     143 && @{$_->{map}{$_->{ready}}};
  5         19  
239              
240 5         5 my $is_terminated;
241              
242 5         5 foreach my $from (keys %{$_->{map}}) {
  5         14  
243             croak 'invalid transition from terminal state detected'
244 10 100       207 if $from eq $_->{term};
245              
246 9         7 foreach my $next (@{$_->{map}{$from}}) {
  9         12  
247 11         22 my $to = $next->{to};
248              
249 11 100       17 if ($to eq $_->{term}) {
250 3         4 $is_terminated = 1;
251 3         4 next;
252             }
253              
254             croak "no subsequent states are reachable from $to"
255 8 100       138 unless exists $_->{map}{$to};
256             }
257             }
258              
259 3 100       114 croak 'no transition defined to terminal state'
260             unless $is_terminated;
261             }
262              
263              
264             1;
265              
266             __END__