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.08';
4              
5 1     1   292219 use strict;
  1         1  
  1         24  
6 1     1   4 use warnings;
  1         1  
  1         23  
7 1     1   401 use parent 'Exporter';
  1         220  
  1         4  
8 1     1   40 use Carp;
  1         2  
  1         56  
9 1     1   511 use Data::Dumper;
  1         4641  
  1         75  
10 1     1   6 use List::Util qw(first reduce);
  1         2  
  1         59  
11 1     1   611 use Storable qw(dclone);
  1         2478  
  1         59  
12 1     1   6 use Type::Utils -all;
  1         1  
  1         14  
13 1     1   1759 use Types::Standard -all;
  1         2  
  1         10  
14 1     1   25856 use Type::Params qw(compile);
  1         10647  
  1         13  
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 11272 my $code = shift;
50              
51             #-----------------------------------------------------------------------------
52             # Define the machine parameters
53             #-----------------------------------------------------------------------------
54 12         47 my %fsm = (
55             ready => undef,
56             term => undef,
57             map => {},
58             );
59              
60 12         15 do {
61 12         16 local $_ = \%fsm;
62 12         24 $code->();
63 11         461 validate();
64             };
65              
66 2         4 my %map = %{$fsm{map}};
  2         9  
67 2         4 my $ready = $fsm{ready};
68 2         3 my $term = $fsm{term};
69 2         9 my $Terminal = declare 'Terminal', as Tuple[Enum[$term], Any];
70              
71             #-----------------------------------------------------------------------------
72             # For each state, compile a type union that matches all 'on' constraints
73             # where that state is the 'to' state. This will be used to validate the state
74             # accumulator after each transition to that state.
75             #-----------------------------------------------------------------------------
76 2         4812 my %final = ($term => $Terminal);
77              
78 2         8 foreach my $from (keys %map) {
79 5         405 my @next = map { $_->{initial} } @{$map{$from}};
  7         17  
  5         11  
80 5     2   53 $final{$from} = declare $from, as reduce { $a | $b } @next;
  2         129  
81             }
82              
83             #-----------------------------------------------------------------------------
84             # Build the transition engine
85             #-----------------------------------------------------------------------------
86 2         177 my @match;
87 2         6 foreach my $from (keys %map) {
88             #---------------------------------------------------------------------------
89             # Create a type constraint that matches each possible initial "from" state.
90             # Use this to build a matching function that calls the appropriate mutator
91             # for that transisiton.
92             #---------------------------------------------------------------------------
93 5         7 foreach my $transition (@{$map{$from}}) {
  5         9  
94 7         10 my $to = $transition->{to};
95 7         6 my $initial = $transition->{initial};
96 7         7 my $with = $transition->{transform};
97 7         8 my $final = $final{$to};
98              
99             push @match, $initial, sub {
100 11     11   1252 my ($from, $input) = @$_;
101 11         23 debug('%s -> %s', $from, $to);
102              
103 11 100       32 do { local $_ = $input; $input = $with->() } if $with;
  8         9  
  8         20  
104 11         61 my $state = [$to, $input];
105              
106 11 100       31 if (defined(my $error = $final->validate($state))) {
107 1 50       244 if (my $explain = $final->validate_explain($state, 'FINAL_STATE')) {
108 1         1975 debug($_) foreach @$explain;
109             }
110              
111 1         11 croak join "\n",
112             sprintf('Transition from %s to %s produced an invalid state.', $from, $to),
113             sprintf('Initial state: %s', explain($_)),
114             sprintf('Final state: %s', explain($_)),
115             sprintf($error);
116             }
117              
118 10         5850 return @$state;
119 7         39 };
120             }
121             }
122              
123 2     1   7 my $default = sub { croak 'no transitions match ' . explain($_) };
  1         183  
124 2         9 my $transform = compile_match_on_type(@match, => $default);
125              
126             #-----------------------------------------------------------------------------
127             # Return function that builds a transition engine for the given input
128             #-----------------------------------------------------------------------------
129             return sub {
130 4     4   3624 my $interactive = shift;
131              
132 4         6 my $state = $ready;
133 4         5 my $done;
134              
135             my $iter = sub (\$) {
136 14 100       1649 return if $done;
137 12         310 ($state, $_[0]) = $transform->([$state, $_[0]]);
138 10         36 $done = $Terminal->check([$state, $_[0]]);
139 10 100       176 wantarray ? ($state, $_[0]) : $state;
140 4         25 };
141              
142 4 100       19 return $iter if $interactive;
143              
144             return sub (\$) {
145 2         5 while (my ($state, $acc) = $iter->($_[0])) {
146             ;
147             }
148              
149 1         5 return $_[0];
150 2         10 };
151 2         2576 };
152             }
153              
154              
155 12     12 1 378 sub ready ($) { assert_in_the_machine(); $_->{ready} = shift }
  10         395  
156 10     10 1 332 sub terminal ($) { assert_in_the_machine(); $_->{term} = shift }
  9         316  
157 1     1 1 6 sub term ($) { goto \&terminal }
158 17     17 1 59 sub to ($;%) { (to => shift, @_) }
159 16     16 1 5138 sub on ($;%) { (on => shift, @_) }
160 3     3 1 22 sub with (&;%) { (with => shift, @_) }
161 0     0 1 0 sub using (&;%) { goto \&with }
162              
163              
164             my $_transition_args;
165              
166             sub transition ($%) {
167 18     18 1 3557 assert_in_the_machine();
168 17   66     1191 $_transition_args ||= compile($Ident, $Ident, $Type, Maybe[CodeRef]);
169              
170 17         2112 my ($arg, %param) = @_;
171 17         63 my ($from, $to, $on, $with) = $_transition_args->($arg, @param{qw(to on with)});
172              
173 17   100     609 $_->{map}{$from} ||= [];
174              
175 17         42 my $name = $on->name;
176 17         115 my $init = declare "${from}_to_${to}_on_${name}", as Tuple[Enum[$from], $on];
177 17         35022 debug("New state: $init");
178              
179 17         28 foreach my $next (@{$_->{map}{$from}}) {
  17         51  
180             croak "transition conflict in $from to $to: $on already matched by $next->{initial}"
181 7 100       622 if $init == $next->{initial};
182             }
183              
184 15         658 my $transition = {
185             to => $to,
186             initial => $init,
187             transform => $with,
188             };
189              
190             # Add this contraint to the list of matches
191 15         18 push @{$_->{map}{$from}}, $transition;
  15         60  
192             }
193              
194             #-------------------------------------------------------------------------------
195             # Throws an error when not within a call to `machine`. When debugging, includes
196             # the full `validate_explain` if the error was due to a type-checking failure.
197             #-------------------------------------------------------------------------------
198             sub assert_in_the_machine {
199 51 100   51 0 530 croak 'cannot be called outside a state machine definition block' unless $_;
200              
201 48 100       115 unless (!defined(my $msg = $Automata->validate_explain($_, '$_'))) {
202 2         1087 debug('Invalid machine state detected: %s', join("\n", map {" -$_"} @$msg));
  9         16  
203 2         207 croak 'invalid machine definition';
204             }
205             }
206              
207             #-------------------------------------------------------------------------------
208             # Emits a debug message preceded by 'DEBUG> ' to STDERR when $DEBUG is true.
209             # Behaves like `warn(sprintf(@_))` in all other respects.
210             #-------------------------------------------------------------------------------
211             sub debug {
212 39 100   39 0 923 return unless $DEBUG;
213 1         3 my ($msg, @args) = @_;
214 1         17 warn sprintf("# DEBUG> $msg\n", @args);
215             }
216              
217             #-------------------------------------------------------------------------------
218             # Alias for Data::Dumper::Dumper with no Indent and Terse output.
219             #-------------------------------------------------------------------------------
220             sub explain {
221 4     4 0 4895 my $state = shift;
222 4         10 local $Data::Dumper::Indent = 0;
223 4         8 local $Data::Dumper::Terse = 1;
224 4         20 Dumper($state);
225             }
226              
227             #-------------------------------------------------------------------------------
228             # Validate sanity as much as possible without strict types and without
229             # guarantees on the return type of transitions.
230             #-------------------------------------------------------------------------------
231             sub validate {
232 11     11 0 23 assert_in_the_machine();
233              
234             croak 'no ready state defined'
235 10 100       1005 unless $_->{ready};
236              
237             croak 'no terminal state defined'
238 9 100       158 unless $_->{term};
239              
240             croak 'terminal state and ready state are identical'
241 8 100       153 if $_->{ready} eq $_->{term};
242              
243             croak 'no transitions defined'
244 7 100       10 unless keys %{$_->{map}};
  7         153  
245              
246             croak 'no transition defined for ready state'
247             unless $_->{map}{$_->{ready}}
248 6 100 66     211 && @{$_->{map}{$_->{ready}}};
  5         26  
249              
250 5         6 my $is_terminated;
251              
252 5         6 foreach my $from (keys %{$_->{map}}) {
  5         12  
253             croak 'invalid transition from terminal state detected'
254 10 100       128 if $from eq $_->{term};
255              
256 9         7 foreach my $next (@{$_->{map}{$from}}) {
  9         13  
257 11         14 my $to = $next->{to};
258              
259 11 100       22 if ($to eq $_->{term}) {
260 4         4 $is_terminated = 1;
261 4         9 next;
262             }
263              
264             croak "no subsequent states are reachable from $to"
265 7 100       102 unless exists $_->{map}{$to};
266             }
267             }
268              
269 3 100       118 croak 'no transition defined to terminal state'
270             unless $is_terminated;
271             }
272              
273              
274             1;
275              
276             __END__