File Coverage

blib/lib/Auto/Mata.pm
Criterion Covered Total %
statement 121 121 100.0
branch 35 36 97.2
condition 3 5 60.0
subroutine 26 26 100.0
pod 8 12 66.6
total 193 200 96.5


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.06';
4              
5 1     1   265337 use strict;
  1         1  
  1         23  
6 1     1   4 use warnings;
  1         1  
  1         23  
7 1     1   363 use parent 'Exporter';
  1         207  
  1         4  
8 1     1   61 use Carp;
  1         1  
  1         56  
9 1     1   511 use Data::Dumper;
  1         4408  
  1         54  
10 1     1   5 use List::Util qw(first reduce);
  1         2  
  1         54  
11 1     1   584 use Storable qw(dclone);
  1         2347  
  1         57  
12 1     1   5 use Type::Utils -all;
  1         1  
  1         13  
13 1     1   1672 use Types::Standard -all;
  1         1  
  1         10  
14 1     1   29749 use Type::Params qw(compile);
  1         11902  
  1         11  
15              
16             our @EXPORT = qw(
17             machine
18             ready
19             terminal
20             term
21             transition
22             to
23             on
24             with
25             );
26              
27             our $DEBUG = $ENV{DEBUG_AUTOMATA};
28              
29             my $Ident = declare 'Ident', as StrMatch[qr/^[A-Z][_0-9A-Z]*$/i];
30             my $State = declare 'State', as Tuple[$Ident, Any];
31             my $Type = declare 'Type', as InstanceOf['Type::Tiny'];
32             coerce $Type, from Undef, via { Any };
33              
34             my $Transition = declare 'Transition', as Dict[initial => $Type, transform => Maybe[CodeRef]];
35             my $Transform = declare 'Transform', as Dict[initial => $Type, transform => CodeRef];
36              
37             my $Automata = declare 'Automata', as Dict[
38             ready => Maybe[$Ident],
39             term => Maybe[$Ident],
40             map => Map[$Ident, Map[$Ident, $Transition]],
41             ];
42              
43              
44             sub machine (&) {
45 12     12 1 9098 my $code = shift;
46              
47             #-----------------------------------------------------------------------------
48             # Define the machine parameters
49             #-----------------------------------------------------------------------------
50 12         13 my %map;
51 12         41 my %fsm = (ready => undef, term => undef, map => \%map);
52              
53 12         11 do {
54 12         13 local $_ = \%fsm;
55 12         21 $code->();
56 11         497 validate();
57             };
58              
59 2         9 my $terminal = declare $fsm{term}, as Tuple[Enum[$fsm{term}], Any];
60 2         3786 my %state = ($fsm{term} => $terminal);
61              
62 2         7 foreach my $from (keys %map) {
63 5         391 my @next = map { $map{$from}{$_}{initial} } keys %{$map{$from}};
  6         17  
  5         10  
64 5     1   43 $state{$from} = declare $from, as reduce { $a | $b } @next;
  1         7  
65             }
66              
67             #-----------------------------------------------------------------------------
68             # Build the transition engine
69             #-----------------------------------------------------------------------------
70 2         206 my @match;
71 2         6 foreach my $from (keys %map) {
72             #---------------------------------------------------------------------------
73             # Create a type constraint that matches each possible initial "from" state.
74             # Use this to build a matching function that calls the appropriate mutator
75             # for that transisiton.
76             #---------------------------------------------------------------------------
77 5         6 foreach my $to (keys %{$map{$from}}) {
  5         9  
78 6         7 my $final = $state{$to};
79 6         6 my $initial = $map{$from}{$to}{initial};
80 6         8 my $with = $map{$from}{$to}{transform};
81              
82             push @match, $initial, sub {
83 6     6   662 my ($from, $input) = @$_;
84 6         18 debug('%s -> %s', $from, $to);
85              
86 6 100       12 do { local $_ = $input; $input = $with->() } if $with;
  3         5  
  3         13  
87 6         30 my $state = [$to, $input];
88              
89 6 100       18 if (defined(my $error = $final->validate($state))) {
90 1 50       182 if (my $explain = $final->validate_explain($state, 'FINAL_STATE')) {
91 1         1180 debug($_) foreach @$explain;
92             }
93              
94 1         7 croak join "\n",
95             sprintf('Transition from %s to %s produced an invalid state.', $from, $to),
96             sprintf('Attempted to move from %s to %s', explain($_), explain($state)),
97             sprintf($error);
98             }
99              
100 5         3671 return @$state;
101 6         30 };
102             }
103             }
104              
105 2     1   16 my $default = sub { croak 'no transitions match ' . explain($_) };
  1         145  
106 2         12 my $transform = compile_match_on_type(@match, => $default);
107              
108             #-----------------------------------------------------------------------------
109             # Return function that builds a transition engine for the given input
110             #-----------------------------------------------------------------------------
111             return sub {
112 3     3   1969 my $state = $fsm{ready};
113 3         4 my $done;
114              
115             sub {
116 8 100       1731 return if $done;
117 7         176 ($state, $_[0]) = $transform->([$state, $_[0]]);
118 5         12 $done = $state eq $fsm{term};
119 5 100       16 wantarray ? ($state, $_[0]) : $state;
120 3         15 };
121 2         1649 };
122             }
123              
124              
125 12     12 1 237 sub ready ($) { assert_in_the_machine(); $_->{ready} = shift }
  10         333  
126 10     10 1 214 sub terminal ($) { assert_in_the_machine(); $_->{term} = shift }
  9         231  
127 1     1 1 8 sub term ($) { goto \&terminal }
128 15     15 1 64 sub to ($;%) { (to => shift, @_) }
129 13     13 1 3806 sub on ($;%) { (on => shift, @_) }
130 2     2 1 22 sub with (&;%) { (with => shift, @_) }
131              
132              
133             my $_transition_args;
134              
135             sub transition ($%) {
136 16     16 1 1833 assert_in_the_machine();
137 15   66     902 $_transition_args ||= compile($Ident, $Ident, $Type, Maybe[CodeRef]);
138              
139 15         2436 my ($arg, %param) = @_;
140 15         47 my ($from, $to, $on, $with) = $_transition_args->($arg, @param{qw(to on with)});
141              
142             croak "transition from state $from to $to is already defined"
143 15 100       738 if exists $_->{map}{$from}{$to};
144              
145 14         70 my $init = declare "${from}_TO_${to}_INITIAL", as Tuple[Enum[$from], $on];
146              
147 14   50     28812 $_->{map}{$from} ||= {};
148              
149 14         70 $_->{map}{$from}{$to} = {
150             initial => $init,
151             transform => $with,
152             };
153             }
154              
155             #-------------------------------------------------------------------------------
156             # Throws an error when not within a call to `machine`. When debugging, includes
157             # the full `validate_explain` if the error was due to a type-checking failure.
158             #-------------------------------------------------------------------------------
159             sub assert_in_the_machine {
160 49 100   49 0 335 croak 'cannot be called outside a state machine definition block' unless $_;
161              
162 46 100       95 unless (!defined(my $msg = $Automata->validate_explain($_, '$_'))) {
163 2         1161 debug('Invalid machine state detected: %s', join("\n", map {" -$_"} @$msg));
  9         19  
164 2         225 croak 'invalid machine definition';
165             }
166             }
167              
168             #-------------------------------------------------------------------------------
169             # Emits a debug message preceded by 'DEBUG> ' to STDERR when $DEBUG is true.
170             # Behaves like `warn(sprintf(@_))` in all other respects.
171             #-------------------------------------------------------------------------------
172             sub debug {
173 17 100   17 0 643 return unless $DEBUG;
174 1         3 my ($msg, @args) = @_;
175 1         14 warn sprintf("# DEBUG> $msg\n", @args);
176             }
177              
178             #-------------------------------------------------------------------------------
179             # Alias for Data::Dumper::Dumper with no Indent and Terse output.
180             #-------------------------------------------------------------------------------
181             sub explain {
182 4     4 0 4700 my $state = shift;
183 4         7 local $Data::Dumper::Indent = 0;
184 4         4 local $Data::Dumper::Terse = 1;
185 4         15 Dumper($state);
186             }
187              
188             #-------------------------------------------------------------------------------
189             # Validate sanity as much as possible without strict types and without
190             # guarantees on the return type of transitions.
191             #-------------------------------------------------------------------------------
192             sub validate {
193 11     11 0 13 assert_in_the_machine();
194              
195             croak 'no ready state defined'
196 10 100       785 unless $_->{ready};
197              
198             croak 'no terminal state defined'
199 9 100       92 unless $_->{term};
200              
201             croak 'terminal state and ready state are identical'
202 8 100       96 if $_->{ready} eq $_->{term};
203              
204             croak 'no transitions defined'
205 7 100       10 unless keys %{$_->{map}};
  7         99  
206              
207             croak 'no transition defined for ready state'
208 6 100       122 unless $_->{map}{$_->{ready}};
209              
210 5         5 my $is_terminated;
211              
212 5         7 foreach my $from (keys %{$_->{map}}) {
  5         11  
213             croak 'invalid transition from terminal state detected'
214 9 100       200 if $from eq $_->{term};
215              
216 8         10 foreach my $to (keys %{$_->{map}{$from}}) {
  8         13  
217 9 100       17 if ($to eq $_->{term}) {
218 2         2 $is_terminated = 1;
219 2         4 next;
220             }
221              
222             croak "no subsequent states are reachable from $to"
223 7 100       120 unless exists $_->{map}{$to};
224             }
225             }
226              
227 3 100       137 croak 'no transition defined to terminal state'
228             unless $is_terminated;
229             }
230              
231              
232             1;
233              
234             __END__