File Coverage

blib/lib/Math/Logic/Predicate.pm
Criterion Covered Total %
statement 236 268 88.0
branch 113 170 66.4
condition 42 84 50.0
subroutine 15 16 93.7
pod 0 11 0.0
total 406 549 73.9


line stmt bran cond sub pod time code
1             package Math::Logic::Predicate;
2              
3 1     1   21518 use Parse::RecDescent;
  1         61402  
  1         9  
4 1     1   50 use Carp;
  1         2  
  1         91  
5              
6 1     1   7 use strict;
  1         20  
  1         3314  
7              
8             our $GRAMMAR = join '', ;
9             our $DEBUG;
10              
11             our $VERSION = 0.03;
12              
13             sub new($)
14             {
15 1     1 0 15 my ($class) = @_;
16             bless {
17             pred => { },
18 125     125   513 nonterm => sub { $_[0] =~ /^[_A-Z]/ },
19 1         15 no_code => undef,
20             parser => undef,
21             } => $class
22             }
23              
24             sub rules(\$) : lvalue
25             {
26 0     0 0 0 shift->{pred}
27             }
28              
29             sub parse(\$$;$)
30             {
31 11     11 0 462 my ($self, $expr, $auto) = @_;
32 11 50 66     69 $self->{parser} ||= new Parse::RecDescent($GRAMMAR) or confess;
33 11         76366 $self->{parser}{LG} = $self;
34 11         16 my $ret;
35 11 100       35 if ($auto) {
36 3         37 $ret = $self->{parser}->auto($expr);
37             }
38             else {
39 8         73 $ret = $self->{parser}->statement($expr);
40             }
41 11 50       1335 if ($ret) {
42 11 50       81 return wantarray ? @$ret : $ret->[0];
43             }
44             return
45 0         0 }
46              
47             sub newproc(\$$$;$$$)
48             {
49 72     72 0 154806 my ($self, $rule, $args, $context, $next, $prev) = @_;
50 72   50     190 $context ||= 'true';
51              
52 72 50       126 unless (grep { $context == $_ } qw(true false and or sub bind)) {
  432         936  
53 0         0 croak "'$context' is not a valid context type";
54             }
55            
56 72         291 my $ret = {
57             context => $context,
58             rule => $rule,
59             args => $args,
60             };
61              
62 72         163 $ret->{next} = $next;
63 72         114 $ret->{fail} = $prev;
64 72 100 33     556 unless ($context eq 'true' || $context eq 'false' || $context eq 'bind') {
      66        
65 66 50 0     152 $ret->{next}{fail} ||= $ret if $next;
66             }
67             $ret
68 72         1954 }
69              
70             sub add(\$$)
71             {
72 13     13 0 22510 my ($self, $proc) = @_;
73              
74 13 100       36 if (ref $proc) {
75            
76 10 50       24 croak "You can't add an undefined rule" unless $proc;
77 10 50       30 croak "You can't add queries to the database" if $proc->{rule} eq '^QUERY';
78 10 50       29 croak "You can't add variable predicates"if $self->{nonterm}($proc->{rule});
79              
80 10 50       41 if (@{$proc->{args}}) {
  10         26  
81 10 100       45 $self->{pred}{$proc->{rule}} = { } unless $self->{pred}{$proc->{rule}};
82             }
83             else {
84 0 0       0 $self->{pred}{$proc->{rule}} = [ ] unless $self->{pred}{$proc->{rule}};
85             }
86 10         35 $self->addproc_h($proc, 0, $self->{pred}{$proc->{rule}});
87 10         208 $proc
88            
89             }
90             else {
91 3         13 $self->parse($proc, 'auto')
92             }
93             }
94              
95             sub retract(\$$)
96             {
97 2     2 0 687 my ($self, $proc) = @_;
98            
99 2 50       11 $proc = $self->parse($proc) unless ref $proc;
100            
101 2         7 my ($pad, $frame) = ( {}, {} );
102 2         13 while ($self->lookup($proc, $frame, $pad, 'delete')) { }
103 2         11 1;
104             }
105              
106             sub addproc_h(\$$$$)
107             {
108 25     25 0 34 my ($self, $proc, $argn, $href) = @_;
109 25 100       61 if (local $_ = $proc->{args}[$argn]) {
110 15 100       26 my $r = $self->{nonterm}($_) ? '_' : $_;
111 15 50       38 unless ($href->{$r}) {
112 15 100       15 if ($argn == $#{$proc->{args}}) { # Last argument
  15         37  
113 10         25 $href->{$r} = [ ];
114             }
115             else {
116 5         12 $href->{$r} = { };
117             }
118 15         26 delete $href->{'^SORT'};
119             }
120 15         43 $self->addproc_h($proc, $argn+1, $href->{$r});
121             }
122             else {
123             # Don't duplicate
124 10 100 66     43 if ($proc->{context} eq 'true' ||
125             $proc->{context} eq 'false') { # Don't duplicate
126 8 50       18 return if grep { $_->{context} eq $proc->{context} } @$href;
  0         0  
127             }
128             # Is this rule directly recursive?
129 10 100       24 if ($proc->{context} eq 'bind') {
130 2         5 my $cptr = $proc->{next};
131 2         9 while ($cptr) {
132 2 50       8 if ($cptr->{rule} eq $proc->{rule}) { #If so...
133 0         0 push @$href, $proc;
134 0         0 return 1;
135             }
136 2         6 $cptr = $cptr->{next};
137             }
138             }
139 10         20 unshift @$href, $proc;
140             }
141 25         33 1;
142             }
143              
144             sub lookup(\$$$$;$)
145             {
146 17     17 0 35 my ($self, $proc, $lse, $pad, $delete) = @_;
147              
148 17         35 my $rule = $proc->{rule};
149 17 50       46 $rule = $pad->{$rule} if $self->{nonterm}($rule);
150              
151 17 50       48 $lse->{fail}++,return if $lse->{fail};
152 17 50 33     93 $lse->{fail}++,return unless $rule && $self->{pred}{$rule};
153              
154 17   100     78 $lse->{pred_stack} ||= [ $self->{pred}{$rule} ];
155 17   100     65 $lse->{iter_stack} ||= [ 0 ];
156 17   100     67 $lse->{bind_stack} ||= [ 0 ];
157 17   100     60 $lse->{pos} ||= 0;
158            
159 17         27 my $pred = $lse->{pred_stack};
160 17         24 my $iter = $lse->{iter_stack};
161 17         26 my $bind = $lse->{bind_stack};
162            
163            
164 17         63 while (@$pred) {
165 47         47 my $p;
166 47 100       66 unless ($lse->{pos} == @{$proc->{args}}) {
  47         113  
167 29         61 $p = $proc->{args}[$lse->{pos}];
168 29 100       72 $pred->[0]{'^SORT'} = [ sort keys %{$pred->[0]} ]
  13         78  
169             unless $pred->[0]{'^SORT'};
170             }
171 47         76 my $state = 'push';
172 47 100       99 $p = exists $pad->{$p} ? $pad->{$p} : $p;
173              
174 47 100       65 if ($lse->{pos} == @{$proc->{args}}) {
  47         123  
175 18 100       27 $state = 'pop' if $iter->[0] == @{$pred->[0]};
  18         49  
176             }
177             else {
178 29 100       60 if ($self->{nonterm}($p)) {
179 9 100       18 $state = 'pop' if $iter->[0] == @{$pred->[0]{'^SORT'}};
  9         32  
180             }
181             else {
182 20         51 my $len = exists($pred->[0]{$p}) + exists($pred->[0]{_});
183 20 100       58 $state = 'pop' if $iter->[0] >= $len;
184             }
185             }
186            
187 47 100       98 if ($state eq 'pop') {
188 19         29 my $free = shift @$bind;
189 19         22 shift @$iter;
190 19         24 shift @$pred;
191 19 100       40 delete $pad->{$free} if $free;
192 19         27 $lse->{pos}--;
193            
194            
195 19 100       73 unless (@$iter) {
196 7         14 $lse->{fail}++;
197 7         20 return;
198             }
199             }
200             else {
201 28         36 my $ind;
202 28         46 my $pi = $iter->[0]++;
203 28 100       67 unless (defined $p) {
    100          
204 10 100       19 if ($delete) {
205 3         11 delete $pred->[1]{'^SORT'};
206 3         4 return splice @{$pred->[0]}, --$iter->[0], 1;
  3         32  
207             }
208             else {
209 7         24 return $pred->[0][$pi];
210             }
211             }
212             elsif ($self->{nonterm}($p)) {
213 7         18 $ind = $pred->[0]{'^SORT'}[$pi];
214             # No binding to anonymous vars
215 7 100 66     48 unless ($p eq '_' || $ind eq '_' ||
216             exists $pad->{$p}) {
217 3         10 $pad->{$p} = $ind;
218 3         7 unshift @$bind, $p;
219             }
220             else { # Still need a frame, though
221 4         11 unshift @$bind, (undef);
222             }
223             }
224             else {
225 11 50       20 if ($pi) {
226 0         0 $ind = '_';
227             }
228             else {
229 11 100       37 $ind = exists $pred->[0]{$p} ? $p : '_';
230             }
231 11         29 unshift @$bind, 0;
232             }
233 18         46 unshift @$pred, $pred->[0]{$ind};
234 18         29 unshift @$iter, 0;
235 18         72 $lse->{pos}++;
236             }
237             }
238             }
239              
240             sub copy_pad(\$$$$$;$) {
241 16     16 0 29 my ($self, $srule, $scon, $drule, $dcon, $bindtrack) = @_;
242            
243 16 50       17 return unless @{$srule->{args}} == @{$drule->{args}};
  16         29  
  16         44  
244            
245             # I want perl6 parallel iteration!!
246 16         25 for (my $i = 0; $i < @{$drule->{args}}; $i++) {
  38         103  
247 22 100       54 if ($self->{nonterm}($drule->{args}[$i])) {
248 12         76 my $bind = $srule->{args}[$i];
249 12 100       25 $bind = $scon->{pad}{$bind} if $self->{nonterm}($bind);
250 12 100 66     100 if (defined $bind && $drule->{args}[$i] ne '_' &&
      100        
251             !exists $dcon->{pad}{$drule->{args}[$i]}) {
252 4         26 $dcon->{pad}{$drule->{args}[$i]} = $bind;
253 4   100     22 $dcon->{stack}[0]{bindings} ||= [ ];
254 4 100       142 push @{$dcon->{stack}[0]{bindings}},
  3         15  
255             $drule->{args}[$i] if $bindtrack;
256             }
257             }
258             }
259 16         28 1;
260             }
261              
262             # Returns a context or undef
263             # Changes are reflected in the pad
264             sub match(\$$;$$)
265             {
266 17     17 0 763 my ($self, $proc, $state, $indent) = @_;
267              
268 17 100       56 $proc = $self->parse($proc) unless ref $proc;
269              
270 17         23 my $cptr; # Pointer to frame of chain
271 17         25 my $res = 0; # Did the last thing executed succeed (1,0)?
272 17         21 my $dir = 1; # Are we going forward or backtracking (1,0)?
273            
274 17   100     87 $state ||= { pad => {}, stack => [] };
275            
276 17 100       75 return $state if $proc->{context} eq 'true';
277 12 50       33 return undef if $proc->{context} eq 'false';
278              
279 12 100 66     74 if ($proc->{context} eq 'bind' && $proc->{code}) {
280 2   100     13 $state->{stack}[0]{bindings} ||= [];
281 2         3 delete $state->{pad}{$_} for @{$state->{stack}[0]{bindings}};
  2         8  
282 2         3 @{$state->{stack}[0]{bindings}} = ();
  2         5  
283              
284 2         5 my @nt = grep { $self->{nonterm}($_) } @{$proc->{args}};
  2         5  
  2         5  
285              
286 2 100       7 unless ($proc->{bindcode}) {
287            
288 1         2 my $ev;
289 1         2 $ev = "package main; no strict; my \%r;\n";
290 1         3 $ev .= 'my ($pad, $stack) = @_;';
291             $ev .= "local \$$_ = \$pad->{$_};"
292 1         8 ."\$r{$_} = \$$_ =~ s/^'//;\n" for @nt;
293 1         3 $ev .= <<'EOC';
294             local $track = !$stack->{track};
295             $stack->{track} = 1;
296             $stack->{local} ||= { };
297             local $local = $stack->{local};
298             my $res = $proc->{code}();
299             EOC
300 1         3 for (@nt) {
301 1         10 $ev .= <
302             if (defined \$$_) {
303             \$$_ = q{'}.\$$_ if \$r{$_} || \$$_ =~ /\\W/;
304             push \@{\$stack->{bindings}}, '$_'
305             unless exists \$pad->{$_};
306             \$pad->{$_} = \$$_;
307             } else {
308             delete \$pad->{$_}
309             }
310             EOC
311             }
312 1         2 $ev .= "\$res\n";
313 1     1   10 $proc->{bindcode} = eval "sub { $ev }";
  1         2  
  1         305  
  1         126  
314 1 50       5 confess $@ if $@;
315             }
316              
317 2         66 $res = $proc->{bindcode}($state->{pad}, $state->{stack}[0]);
318 2 50       13 return $res ? $state : undef
319             }
320            
321 10 100       59 if ($state->{stack}[0]{ptr}) { # Anything meaningful on the stack?
322 3 50       10 print "$indent Loading stack...\n" if $DEBUG;
323 3         4 delete $state->{pad}{$_} for @{$state->{stack}[0]{bindings}};
  3         17  
324 3         6 @{$state->{stack}[0]{bindings}} = ();
  3         9  
325 3         8 $cptr = $state->{stack}[0]{ptr};
326 3         6 $dir = 0;
327             }
328             else { # Put something there
329             # $proc is the name of the rule; we want $proc->{next}
330 7         30 $state->{stack}[0]{ptr} = $cptr = $proc->{next};
331             }
332              
333 10         28 while ($cptr) {
334 13         29 $state->{stack}[0]{ptr} = $cptr; # Tell the stack where we are
335            
336             # If we're backtracking, and we skipped on the forward, skip back too
337 13 0 33     46 goto skip if $cptr->{context} eq 'or' and $state->{stack}[0]{skip}
      33        
338             and not $dir;
339            
340 13 0 33     37 goto skip if $cptr->{context} eq 'sub'
      33        
341             and not $dir and not $state->{stack}[0]{last};
342 13 50 33     58 goto skip if $cptr->{context} eq 'sub' && not $res;
343              
344             # On forward success in an or chain, skip the current rule
345 13 0 33     39 goto skip if $cptr->{context} eq 'or' and $res and $dir
      33        
      0        
346             and $state->{stack}[0]{skip} = 1;
347            
348 13         28 $state->{stack}[0]{skip} = 0;
349            
350             # In true context, just go forward
351 13 50       34 if ($cptr->{context} eq 'true') {
352 0         0 $res = $dir = 1;
353 0         0 goto retry;
354             }
355              
356             # In false context, just go backward (duh)
357 13 50       30 if ($cptr->{context} eq 'false') {
358 0         0 $res = $dir = 0;
359 0         0 goto retry;
360             }
361            
362             # If we don't have something to try, try to get something to try
363 13         26 my $try = $state->{stack}[0]{rule};
364 13         30 my $frame = $state->{stack}[0]{frame};
365 13 100       25 unless ($frame) {
366 0         0 print "$indent Look: $cptr->{rule}(",
367 12 50       27 join(', ', map { "$_($state->{pad}{$_})" } @{$cptr->{args}}),
  0         0  
368             ")\n" if $DEBUG;
369 12         53 $try = $self->lookup($cptr, $state->{stack}[0], $state->{pad});
370             # Fail entirely if we couldn't find anything new
371 12 100       28 unless ($try) {
372 5 50       13 print "$indent Lost\n" if $DEBUG;
373 5         7 $res = 0;
374 5         84 goto retry;
375             }
376 7 50       21 print "$indent Find: $try->{rule}(", join(', ', @{$try->{args}}),
  0         0  
377             ")\n" if $DEBUG;
378              
379 7 100       27 if ($try->{context} eq 'bind') { # Only if it's complex
380 2         7 $state->{stack}[0]{rule} = $try;
381 2         16 $state->{stack}[0]{frame} = $frame = { stack => [], pad => {} };
382             }
383             }
384            
385             # Give them variables they need and we have
386 8         34 $self->copy_pad($cptr, $state => $try, $frame);
387            
388 0         0 print "$indent Try: $cptr->{rule}(", join(', ',
389 8 50       18 map { $_ . "($state->{pad}{$_})" } @{$cptr->{args}}), ")\n"
  0         0  
390             if $DEBUG;
391            
392 8 50       82 unless ( $res = ! !$self->match($try, $frame, "$indent ") ) {
393 0 0       0 print "$indent Fail\n" if $DEBUG;
394 0         0 undef $state->{stack}[0]{frame}; # Clear the frame
395 0         0 next; # Try again
396             }
397            
398            
399 8 50       30 $dir = 1 if $res;
400              
401             # Get their variables if they bound any we want
402 8         22 $self->copy_pad($try, $frame => $cptr, $state, 'bind');
403            
404 0         0 print "$indent Match: $cptr->{rule}(", join(', ',
405 8 50       21 map { $_ . "($state->{pad}{$_})" } @{$cptr->{args}}), ")\n"
  0         0  
406             if $DEBUG;
407            
408             retry:
409 13 50       35 if ($cptr->{context} eq 'sub') {
410 0 0       0 if ($dir) {
411 0 0 0     0 if ($res && $state->{stack}[1]{last}) {
    0          
412 0         0 $dir = $res = 0;
413             }
414             elsif (!$state->{stack}[1]{last}) {
415 0         0 $dir = $res = 0;
416 0 0 0     0 if ($cptr->{next} &&
      0        
417             ($cptr->{next}{context} eq 'or'
418             || $cptr->{next}{context} eq 'sub')) {
419 0 0       0 $dir = 1 unless $state->{stack}[0]{fail} > 1;
420             }
421             }
422             else {
423 0         0 $dir = $res = 1;
424             }
425             }
426             }
427             else {
428 13         20 $dir = $res;
429 13 50 33     59 if ($cptr->{next} &&
      66        
430             ($cptr->{next}{context} eq 'or'
431             || $cptr->{next}{context} eq 'sub')) {
432 0 0       0 $dir = 1 unless $state->{stack}[0]{fail} > 1;
433             }
434             }
435              
436             skip:
437 13 100       22 if ($dir) { # If we're going forward
438 8         23 $state->{stack}[0]{last} = $res;
439              
440 8         14 $cptr = $cptr->{next};
441 8         9 unshift @{$state->{stack}}, { }; # Establish new stack frame
  8         33  
442             }
443             else { # We're going backward
444 5         9 $cptr = $cptr->{fail};
445 5         8 for (@{$state->{stack}[0]{bind_stack}}) {
  5         18  
446 0         0 delete $state->{pad}{$_};
447             }
448 5         7 @{$state->{stack}[0]{bind_stack}} = ();
  5         13  
449 5         11 shift @{$state->{stack}}; # Clear the frame
  5         8  
450             # Unbind any variables in the new frame
451             # in order to rebind them this run.
452 5         16 delete $state->{pad}{$_} for @{$state->{stack}[0]{bindings}};
  5         17  
453 5         11 @{$state->{stack}[0]{bindings}} = ();
  5         19  
454             }
455             }
456              
457 10         11 shift @{$state->{stack}};
  10         22  
458 10 100       43 $res ? $state : undef;
459             }
460              
461             sub get(\$$$) {
462 4     4 0 23 my ($self, $iter, $sym) = @_;
463 4 50       13 if (exists $iter->{pad}{$sym}) {
464 4         10 my $ret = $iter->{pad}{$sym};
465 4         13 $ret =~ s/^'//;
466 4         22 $ret
467             }
468             else {
469             undef
470 0           }
471             }
472              
473             1
474              
475             __DATA__