File Coverage

blib/lib/Grammar/Graph/Simplify.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             #####################################################################
2             # Grammar::Graph
3             #####################################################################
4             package Grammar::Graph::Simplify;
5 1     1   42547 use 5.012000;
  1         5  
  1         40  
6 1     1   8 use strict;
  1         2  
  1         39  
7 1     1   6 use warnings;
  1         7  
  1         38  
8 1     1   655 use Grammar::Graph;
  0            
  0            
9             use Grammar::Formal;
10             use Algorithm::ConstructDFA::XS 0.13;
11             use List::UtilsBy qw/partition_by/;
12             use List::MoreUtils qw/uniq/;
13             use List::Util qw/shuffle sum max/;
14             use Storable qw/freeze thaw/;
15             use Graph::SomeUtils qw/:all/;
16              
17             local $Storable::canonical = 1;
18              
19             our $VERSION = '0.02';
20              
21             our %EXPORT_TAGS = ( 'all' => [ qw(
22            
23             ) ] );
24              
25             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
26              
27             our @EXPORT = qw(
28             );
29              
30             sub Grammar::Graph::fa_merge_equivalent_vertices {
31             my ($g, $start_vertex, $final_vertex) = @_;
32            
33             die if $g->get_graph_attribute('ran_fa_merge_equivalent_vertices');
34             $g->set_graph_attribute('ran_fa_merge_equivalent_vertices', 'yes');
35              
36             my $signature = sub {
37             my ($g, $label) = @_;
38            
39             my $symbols = $g->get_graph_attribute('symbol_table');
40            
41             if (not defined $label) {
42             return;
43             } elsif ($label->isa('Grammar::Graph::StartOf')) {
44             return "start " . $symbols->{ $label->of }{shortname};
45             } elsif ($label->isa('Grammar::Graph::FinalOf')) {
46             return "final " . $symbols->{ $label->of }{shortname};
47             } elsif ($label->isa('Grammar::Graph::Prefix')) {
48             return "prefix " . $label->link;
49             } elsif ($label->isa('Grammar::Graph::Suffix')) {
50             return "suffix " . $label->link;
51             } elsif ($label->isa('Grammar::Formal::CharClass')) {
52             die "Bad label" if $label->spans->empty;
53             return '' . $label->spans;
54             } elsif ($label->isa('Grammar::Formal::Reference')) {
55             ...
56             } elsif ($label->isa('Grammar::Formal::Empty')) {
57             return;
58             } else {
59             ...
60             }
61             };
62            
63             my $get_classes = sub {
64             my ($start_vertex, $final_vertex, $sub) = @_;
65              
66             my $dfa = Algorithm::ConstructDFA::XS::construct_dfa_xs(
67             start => [ $start_vertex ],
68             is_accepting => sub { grep { $_ eq $final_vertex } @_ },
69             is_nullable => sub {
70             my $label = $g->get_vertex_attribute($_[0], 'label');
71             return 1 unless defined $label;
72             return 1 if ref $label eq 'Grammar::Formal::Empty';
73             return 0;
74             },
75             successors => $sub,
76             get_label => sub {
77             my $label = $g->get_vertex_attribute($_[0], 'label');
78             return unless defined $label;
79             return if ref $label eq 'Grammar::Formal::Empty';
80             return $signature->($g, $label);
81             return;
82             },
83             );
84            
85             my %delta;
86             for my $s (keys %$dfa) {
87             $delta{$_}->{$s}++ for @{ $dfa->{$s}{Combines} };
88             }
89              
90             my %h = partition_by {
91             join ' ', sort keys %{ $delta{$_} }
92             } keys %delta;
93            
94             return values %h;
95             };
96            
97             while (1) {
98             my $changed = 0;
99             my @fwd = $get_classes->($start_vertex, $final_vertex, sub { $g->successors($_[0]); });
100             my @bck = $get_classes->($final_vertex, $start_vertex, sub { $g->predecessors($_[0]); });
101             my @eq;
102            
103             for my $x (@fwd, @bck) {
104             push @eq, grep { @$_ > 1 } values %{{ partition_by {
105             my $label = $g->get_vertex_attribute($_, 'label');
106             return 'prefix' if $label->isa('Grammar::Graph::Prefix');
107             return 'suffix' if $label->isa('Grammar::Graph::Suffix');
108             return $signature->($g, $label) // '';
109             } @$x }};
110             }
111              
112             my %cappa;
113            
114             for my $group (@eq) {
115             my $label0 = $g->get_vertex_attribute($group->[0], 'label');
116             next unless $label0->isa('Grammar::Graph::Prefix')
117             or $label0->isa('Grammar::Graph::Suffix');
118            
119             for my $v1 (@$group) {
120             for my $v2 (@$group) {
121             $cappa{$v1}->{$v2}++;
122             }
123             }
124             }
125            
126             my %ren;
127             for my $v1 (sort keys %cappa) {
128             for my $v2 (sort keys %{$cappa{$v1}}) {
129             my $label1 = $g->get_vertex_attribute($v1, 'label');
130             my $label2 = $g->get_vertex_attribute($v2, 'label');
131             my ($p1, $s1) = split/ # /, $label1->link, 2;
132             my ($p2, $s2) = split/ # /, $label2->link, 2;
133             next if $p1 eq $p2;
134             next if $s1 eq $s2;
135             next unless $cappa{$p1}->{$p2} and
136             $cappa{$s1}->{$s2};
137            
138             $ren{"$p1 $s1"} = $ren{"$p2 $s2"} // [$p2, $s2];
139             }
140             }
141            
142             my $replace = sub {
143             my ($g, $goes, $stays) = @_;
144            
145             return if $goes eq $stays;
146              
147             unless ($g->has_vertex($goes)) {
148             # warn $goes . " not in graph";
149             return;
150             }
151              
152             unless ($g->has_vertex($stays)) {
153             # warn "Cannot replace $goes by $stays because $stays does not exist";
154             return;
155             }
156              
157             # warn "replacing $goes by $stays\n";
158            
159             for my $p ($g->predecessors($goes)) {
160             $g->add_edge($p, $stays);
161             }
162             for my $s ($g->successors($goes)) {
163             $g->add_edge($stays, $s);
164             }
165              
166             graph_delete_vertex_fast($g, $goes);
167            
168             ###############################################################
169             # Note that this makes no effort to adjust the ->link attribute
170             # of the vertex, so while above we rely on the contents of it
171             # to be accurate, it cannot be relied upon after running this
172             # step. Ideally that should be fixed to maintain invariants.
173             ###############################################################
174             };
175              
176             for my $k (sort keys %ren) {
177             my ($p1, $s1) = split/ /, $k, 2;
178             my ($p2, $s2) = @{ $ren{$k} };
179             $replace->($g, $p1, $p2);
180             $replace->($g, $s1, $s2);
181             next unless $p1 ne $p2 or $s1 ne $s2;
182             $changed += 1;
183             }
184            
185             next if $changed;
186            
187             for my $group (@eq) {
188             my $label = $g->get_vertex_attribute($group->[0], 'label');
189             next unless $label;
190             next if $label->isa('Grammar::Graph::Prefix');
191             next if $label->isa('Grammar::Graph::Suffix');
192              
193             for (my $ix = 1; $ix < @$group; ++$ix) {
194             $replace->($g, $group->[$ix], $group->[0]);
195             $changed += 1;
196             }
197             }
198              
199             last unless $changed;
200             }
201             }
202              
203              
204             1;
205              
206             __END__
207              
208             =head1 NAME
209              
210             Grammar::Graph::Simplify - Simplify Grammar::Graph objects
211              
212             =head1 SYNOPSIS
213              
214             use Grammar::Graph;
215             use Grammar::Graph::Simplify;
216             my $g = Grammar::Graph->from_grammar_formal($formal);
217             ...
218             $g->fa_merge_equivalent_vertices($start_vertex, $final_vertex);
219              
220             =head1 DESCRIPTION
221              
222             Extension methods for L<Grammar::Graph> objects that simplify
223             Grammars when possible.
224              
225             =head1 METHODS
226              
227             =over
228              
229             =item C<fa_merge_equivalent_vertices($start_vertex, $final_vertex)>
230              
231             This method is added to L<Grammar::Graph> objects and when called it
232             attempts to merge equivalent vertices in the object between the given
233             C<$start_vertex> and C<$final_vertex>. Ideally, the start vertex does
234             not have incoming edges, and the final vertex does not have outgoing
235             edges. The code is untested for when they do. It relies on being able
236             to determine whether two labeled vertices have an equivalent label and
237             there is currently no extension functionality to consider any but the
238             standard labels. It dies when there are unrecognised labels. For the
239             sentinel labels C<Grammar::Graph::Prefix> and C<Grammar::Graph::Suffix>
240             vertices are merged only when matching pairs are equivalent.
241              
242             The code relies on the C<link> attributes of sentinel labels to determine
243             which pairs are matching pairs, but then does not make any attempt to
244             correct the C<link> attributes, so it can be run only once on a given
245             L<Grammar::Graph> object. The code dies if an attempt is made to run the
246             method a second time (it uses a graph attribute to maintain this state).
247              
248             =back
249              
250             =head1 EXPORTS
251              
252             None.
253              
254             =head1 AUTHOR / COPYRIGHT / LICENSE
255              
256             Copyright (c) 2014 Bjoern Hoehrmann <bjoern@hoehrmann.de>.
257             This module is licensed under the same terms as Perl itself.
258              
259             =cut