File Coverage

blib/lib/Algorithm/ConstructDFA.pm
Criterion Covered Total %
statement 139 143 97.2
branch 35 42 83.3
condition 17 23 73.9
subroutine 21 21 100.0
pod 1 1 100.0
total 213 230 92.6


line stmt bran cond sub pod time code
1             package Algorithm::ConstructDFA;
2            
3 2     2   109337 use 5.012000;
  2         8  
  2         81  
4 2     2   13 use strict;
  2         5  
  2         218  
5 2     2   10 use warnings;
  2         9  
  2         71  
6 2     2   10 use base qw(Exporter);
  2         14  
  2         689  
7 2     2   3818 use Storable qw/freeze thaw/;
  2         14349  
  2         337  
8 2     2   2774 use List::UtilsBy qw/partition_by/;
  2         6620  
  2         812  
9 2     2   9116 use List::MoreUtils qw/uniq/;
  2         4724  
  2         216  
10 2     2   2557 use Data::AutoBimap;
  2         1333  
  2         84  
11 2     2   2765 use Memoize;
  2         10995  
  2         6452  
12            
13             our $VERSION = '0.03';
14            
15             our %EXPORT_TAGS = ( 'all' => [ qw(
16             construct_dfa
17             ) ] );
18            
19             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
20            
21             our @EXPORT = qw(
22             construct_dfa
23             );
24            
25             local $Storable::canonical = 1;
26            
27             sub _memoizess {
28 90     90   145 my ($sub) = @_;
29 90         134 my %cache;
30             return sub {
31 18965     18965   29700 my ($s) = @_;
32 18965 100       71349 if (not exists $cache{$s}) {
33 736         1294 $cache{$s} = $sub->($s);
34             }
35 18965         332676 return $cache{$s};
36 90         534 };
37             }
38            
39             sub _get_graph {
40 30     30   73 my ($roots, $labelf, $nullablef, $successorsf, $acceptingf) = @_;
41            
42 30         404 my $m = Data::AutoBimap->new();
43            
44 30     364   710 my $label = _memoizess(sub { $labelf->($m->n2s($_[0])) });
  364         1256  
45            
46             my $successors = memoize(sub {
47 364     364   9587 map { $m->s2n($_) } $successorsf->($m->n2s($_[0]))
  805         35531  
48 30         270 });
49            
50             my $accepting = sub {
51 785     785   9500 !!$acceptingf->(map { $m->n2s($_) } @_)
  6853         51167  
52 30         20567 };
53            
54 30         62 my %nullable;
55            
56             my $nullable = _memoizess(sub {
57 364     364   1299 !!$nullablef->($m->n2s($_[0]));
58 30         172 });
59            
60             my $all_reachable_and_self = _memoizess(sub {
61 8     8   20 my ($v) = @_;
62 8         18 my %seen;
63 8         23 my @todo = ($v);
64 8         42 while (@todo) {
65 81         860 my $c = pop @todo;
66 81 100       296 next if $seen{$c}++;
67 63 100       209 push @todo, $successors->($c) if $nullable->($c);
68             }
69 8         295 [keys %seen];
70 30         451 });
71            
72             my $all_reachable_and_self_many = sub {
73 1167     1167   35905 my %seen;
74 1167         3173 my @todo = (@_);
75 1167         3393 while (@todo) {
76 18941         152889 my $c = pop @todo;
77 18941 100       89997 next if $seen{$c}++;
78 11980 100       32670 push @todo, $successors->($c) if $nullable->($c);
79             }
80 1167         35836 keys %seen;
81 30         212 };
82            
83 161         221 my $start = [
84 8         30 sort { $a cmp $b }
85             uniq map {
86 30 100       677 $nullable->($_) ? @{ $all_reachable_and_self->($_) } : $_
  30         139  
87             }
88 30         92 map { $m->s2n($_) }
89             @$roots
90             ];
91            
92 30         589 my $start_s = join ' ', @$start;
93            
94 30         149 my @todo = ($start);
95 30         76 my %seen;
96             my $dfa;
97            
98 0         0 my @accepting_dfa_states;
99 0         0 my %predecessors;
100            
101 30         100 while (@todo) {
102 1197         1979 my $src = pop @todo;
103 1197         1551 my @src = @{ $src };
  1197         5723  
104 1197         8270 my $src_s = join ' ', @src;
105 1197 100       9152 next if $seen{$src_s}++;
106            
107 400         1045 my $src_accepts = $accepting->(@src);
108 400 100       1500 push @accepting_dfa_states, $src_s if $src_accepts;
109            
110 3442     3442   19191 my %p = partition_by { $label->($_) }
111 400         2683 grep { defined $label->($_) } @src;
  3442         6976  
112            
113 400         5214 while (my ($k, $v) = each %p) {
114 32776         42681 my @dst = sort { $a cmp $b } uniq
  3442         171612  
115 1167         2312 $all_reachable_and_self_many->(map { $successors->($_) } @$v);
116            
117 1167         6339 push @todo, \@dst;
118 1167         3939 my $dst_s = join ' ', @dst;
119 1167         3749 $dfa->{$src_s}->{$k} = $dst_s;
120 1167         11219 $predecessors{$dst_s}->{$src_s}++;
121             }
122             }
123            
124 30         63 my %reachable = do {
125 30         41 my %seen;
126 30         135 my @todo = @accepting_dfa_states;
127 30         115 while (@todo) {
128 1241         1628 my $c = pop @todo;
129 1241 100       5282 next if $seen{$c}++;
130 355         521 push @todo, keys %{ $predecessors{$c} };
  355         1958  
131             }
132 30         143 map { $_ => 1 } keys %seen;
  355         834  
133             };
134            
135 30         283 my $o = Data::AutoBimap->new(start => 0);
136            
137             # Ensure that DFA state 0 is the one that corresponds to no
138             # vertices in the input graph. This is an API convention and
139             # does not have significance beyond that.
140 30         600 my $r = { $o->s2n('') => {
141             Combines => [],
142             Accepts => $accepting->()
143             } };
144            
145             # Ensure start state is 1 also as a convention
146 30         137 $o->s2n($start_s);
147            
148 30         403 while (my ($src, $x) = each %$dfa) {
149            
150             # Merge dead states
151 377 100       1208 $src = '' unless $reachable{$src};
152            
153 377         1902 my @src_combines = map { $m->n2s($_) } split/ /, $src;
  3411         18780  
154 377   100     4221 $r->{$o->s2n($src)}{Combines} //= \@src_combines;
155 0   50     0 $r->{$o->s2n($src)}{Combines} = [ sort { $a cmp $b }
  22         119  
156 377 100       4332 uniq (@{$r->{$o->s2n($src)}{Combines} // []}, @src_combines) ]
157             if $src eq '';
158            
159 377   100     1326 $r->{$o->s2n($src)}{Accepts} //=
160             0 + $accepting->(split/ /, $src);
161            
162 377         2083 while (my ($k, $dst) = each %{$x}) {
  1544         26693  
163            
164 1167 100       2955 $dst = '' unless $reachable{$dst};
165            
166 1167         3001 $r->{$o->s2n($src)}{NextOver}{$k} = $o->s2n($dst);
167            
168 1167 100 100     17661 if ((not defined $r->{$o->s2n($dst)}{Combines}) or $dst eq '') {
169 333         4035 my @dst_combines = map { $m->n2s($_) } split/ /, $dst;
  2203         12461  
170 333   100     10924 $r->{$o->s2n($dst)}{Combines} //= \@dst_combines;
171 0   50     0 $r->{$o->s2n($dst)}{Combines} = [ sort { $a cmp $b }
  120         312  
172 333 100       3711 uniq (@{$r->{$o->s2n($dst)}{Combines} // []}, @dst_combines) ]
173             if $dst eq '';
174             }
175            
176 1167   100     12270 $r->{$o->s2n($dst)}{Accepts} //=
177             0 + $accepting->(split/ /, $dst);
178             }
179             }
180            
181 30         2946 return $r;
182             }
183            
184             sub construct_dfa {
185 30     30 1 24080394 my (%o) = @_;
186            
187 30 50       199 die unless ref $o{is_nullable};
188 30 50 33     258 die unless ref $o{is_accepting} or exists $o{final};
189 30 50       143 die unless ref $o{successors};
190 30 50       126 die unless ref $o{get_label};
191 30 50       110 die unless exists $o{start};
192 30 50 33     127 die if ref $o{is_accepting} and exists $o{final};
193            
194 30 50       322 if (exists $o{final}) {
195 30         43 my %in_final = map { $_ => 1 } @{ $o{final} };
  30         243  
  30         80  
196             $o{is_accepting} = sub {
197 785     785   21061 grep { $in_final{$_} } @_
  6853         13032  
198 30         238 };
199             }
200            
201 30         160 _get_graph($o{start}, $o{get_label}, $o{is_nullable},
202             $o{successors}, $o{is_accepting});
203            
204             }
205            
206            
207             1;
208            
209             __END__