File: Logic.pm

package info (click to toggle)
fai 6.5.4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,076 kB
  • sloc: sh: 6,720; perl: 5,626; makefile: 138
file content (329 lines) | stat: -rw-r--r-- 9,569 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
=head1 NAME

Eval::Logic - Evaluate simple logical expressions from a string.

=head1 DESCRIPTION

With this module simple logical expressions from strings which  use logical
operators like and, or, not and the ternary operator can be evaluated.

This module was created because I wanted to be able to use a simple argument
validator which can be fully configured from YAML.  This module allows a
specification like "we require a_value and some_other_value, or a
a_third_option should be specified" to be expressed as a simple string
"(a_value && some_other_value) || a_third_option".

The module uses eval() and while it does take care to check for anything
other than a logical expression you should take a lot of care when
evaluating expressions from an untrusted source (in fact, I would not
recommend doing that at all).

=head1 SYNOPSIS

 $l = Eval::Logic->new ( '(a || b) && c' );
 $l->evaluate ( a => 1, b => 0, c => 1 ); 	# returns 1 for true
 $l->evaluate ( a => 1, b => 1, c => 0 );	# returns 0 for false
 $l->evaluate_if_true ( 'a', 'b' );		# an alternative for that second example
 $l->evaluate_if_false ( 'c' );			# and another alternative

=head1 METHODS

=cut

package Eval::Logic;

use strict;
use warnings;

use Carp;
use Symbol;
use utf8;

# Forbidden list if truth value names; these are Perl operators with regular
# names that cannot be overridden by using 'use subs'.
our @forbidden_tv_names = qw( or and not xor );

=head2 new (constructor)

 $l = Eval::Logic->new ( 'a && b' );
 
Create a new instance of Eval::Logic. Optionally an expression can be
specified which is immediately loaded in the object, see the expression
method for more information about the expression syntax.

=cut

sub new {
  my $class = shift;
  my $self = bless { undef_default => undef }, $class;
  $self->expression ( @_ ) if ( @_ );
  return $self;
}

=head2 expression

 $expression = $l->expression;
 $l->expression ( 'a && b' );
 
If called without an argument the current expression is returned, otherwise
the current expression in this object is replaced by whatever was specified. 
If multiple strings are specified they are combined in a single expression
that will require all individual expressions to be true.

An expression is a string in which the truth values are specified as simple
(bare) words which can contain letters, digits and underscores and which
must not begin with a digit.  In addition to this, the Perl logical
operators && (and), || (or), ! (not) can be used, as well as the ternary ?:
operator and parentheses. Whitespace is ignored.

The barewords TRUE and FALSE have a special meaning which you can probably
guess.

The method will croak if the expression provided is invalid.

=cut

sub expression {
  my $self = shift;
  if ( @_ ) {

    my $exp = @_ > 1 ? join ( ' && ', map { '(' . $_ . ')' } @_ ) : $_[0];

    my %tv;
    foreach my $v ( 
      split /			# split on anything that cannot be a truth value:
        (?:
          &&	|		# and operator,
          \|\|	|		# or operator,
          !	|		# not operator,
          \?	|		# the first part of the ternary operator,
          \:	|		# the second part of the ternary operator,
          \(	|		# opening parentheses,
          \)	|		# closing parentheses,
          \s			# any whitespace
        )+
      /x, $exp
    ) {
      if ( $v ) {
        next if (( $v eq 'TRUE' ) || ( $v eq 'FALSE' ));
        if ( grep { $v eq $_ } @forbidden_tv_names ) {
          croak "Invalid truth value in expression, named identical to Perl reserved word: '$v'";
        } elsif ( $v =~ /^[a-zA-Z_][a-zA-Z_0-9Ö]*$/ ) {
          $tv{$v} = undef;
        } else {
          croak "Syntax error or invalid truth value in expression: '$v'";
        }
      }
    }

    # Test the expression by evaluating it.
    $self->_eval ( $exp, %tv );
    
    # If we're here, the expression checked out.
    $self->{tv} = [ keys %tv ];
    $self->{exp} = $exp;
    
  } else {
    return $self->{exp};
  }
}

=head2 evaluate

 $outcome = $l->evaluate ( a => 1, b => 0 );
 
Evaluate the logic expression given the specified truth values. If no
default for undefined truth values is specified and some truth values are
not defined or not present, a warning is given.

The outcome is returned as 1 for true or 0 for false.

=cut

sub evaluate {
  my $self = shift;
  my %specified_tv = @_;
  
  croak 'TRUE or FALSE specified as a variable truth value' if (( exists $specified_tv{TRUE} ) || ( exists $specified_tv{FALSE} ));
  
  if ( defined $self->{exp} ) {
    my %tv;
    foreach my $v ( @{$self->{tv}} ) {
      if ( defined $specified_tv{$v} ) {
        $tv{$v} = $specified_tv{$v};
      } elsif ( defined $self->{undef_default} ) {
        $tv{$v} = $self->{undef_default};
      } else {
        carp (( exists $specified_tv{$v} ? 'Undefined' : 'Unspecified' ) . " truth value $v defaults to false" );
        $tv{$v} = 0;
      }
    }
    return $self->_eval ( $self->{exp}, %tv );
  } else {
    carp "No expression, returning false";
    return 0;
  }
}

=head2 evaluate_if_false

 $outcome = $l->evaluate_if_false ( 'a' );
 
Evaluate the logic expression given the specified values to be false, and
all other values to be true.  This is a shortcut to the evaluate method.

=cut

sub evaluate_if_false { shift->_eval_if ( 0, @_ ) }

=head2 evaluate_if_true

 $outcome = $l->evaluate_if_true ( 'b' );
 
Evaluate the logic expression given the specified values to be true, and all
other values to be false.  This is a shortcut to the evaluate method.

=cut

sub evaluate_if_true { shift->_eval_if ( 1, @_ ) }

=head2 truth_values

 @truth_values = $l->truth_values;
 
Return a list of all variable truth values which are present in the
currently loaded expression.

=cut

sub truth_values {
  my $self = shift;
  if ( defined $self->{exp} ) {
    return @{$self->{tv}};
  } else {
    carp "No expression, returning empty list";
    return ();
  }
}

=head2 undef_default

 $default = $l->undef_default;
 $l->undef_default ( $default );

Returns the current default for undefined truth values if specified without
an argument, or sets the default value to the specified argument.  If you
want undefined values to default to false you must explicitly call this
method with an argument that is defined and evaluates to false to suppress
warnings given about undefined values by the evaluate method.

=cut

sub undef_default {
  my $self = shift;
  if ( @_ ) {
    $self->{undef_default} = $_[0];
  } else {
    return $self->{undef_default};
  }
}
  
#
# The _eval method does the work: it creates a piece of Perl code and then
# evaluates it. It will get a bit dirty in here.
#

sub _eval {
  my $self = shift;
  my ( $exp, %tv ) = @_;
  
  # Make sure TRUE and FALSE always mean what they say.
  $tv{TRUE} = 1;
  $tv{FALSE} = 0;
  
  # Generate a piece of code in a 'scratch' package which we will clean
  # before using it.
  my $code = '';

  # To parse any error messages we count the number of lines added. 
  my $our_lines = 0;
  
  # Begin with the package declaration and declare the subroutine names
  # we're using to prevent them from calling core subroutines.
  $code .= 'package ' . __PACKAGE__ . "::Scratch;\n"; $our_lines++;
  $code .= 'use subs qw(' . join ( ' ', keys %tv ) . ");\n"; $our_lines++;

  # Generate a constant subroutine for every value.
  while ( my ( $name, $truth ) = each %tv ) {
    
    # For true we use 1, for false we use an empty list because that will
    # always evaluate to false, even in list context (think about stuff like
    # '(FALSE)' which must evaluate to false, and not to a list of one
    # element).
    
    $code .= 'sub ' . $name . '(){' . ( $truth ? '1' : '()' ) . "}\n";
    $our_lines++;
  }
  
  # Finally we add the expression itself.
  $code .= $exp . "\n;";
  
  # Reset the package namespace and evaluate the generated code block.
  Symbol::delete_package __PACKAGE__ . '::Scratch';
  my $outcome = eval $code ? 1 : 0;

  if ( my $error = $@ ) {
  
    # Some error messages are changed on the fly to make them clearer...
    # hopefully.
    $error =~ s/Too many arguments for @{[__PACKAGE__]}::Scratch::(\S+)/Truth value '$1' not followed by boolean operator/;
  
    # An error occurred while evaluating our code; try to determine the
    # location of the error.
    if ( $error =~ /(at \(eval [0-9]+\) line ([0-9]+))/ ) {
      my ( $location_text, $error_line ) = ( $1, $2 );
      $error_line -= $our_lines;
      if ( $error_line > 0 ) {		# the error was in the expression, change the error message to be more descriptive
        $error =~ s/\Q$location_text\E/at line $error_line in logical expression/;
        croak $error;
      } else {				# woops
        croak "Eval::Logic internal error while evaluating expression: $error";
      }
    }
    
    # If we're still here we just repeat whatever error we got.
    croak $error;
    
  }
  
  # Make sure we always return 1 for true and 0 for false.
  return $outcome ? 1 : 0;
  
}

#
# General implementation of evaluate_if_(true|false)
#

sub _eval_if {
  my $self = shift;
  my $truth = shift;
  my @values = @_;
  my %tv = map { $_ => $truth ? 0 : 1 } @{$self->{tv}};
  foreach ( @values) { $tv{$_} = $truth }
  return $self->evaluate ( %tv );
}

=head1 AUTHOR

Sebastiaan Hoogeveen <pause-zebaz@nederhost.nl>

=head1 COPYRIGHT

Copyright (c) 2016 Sebastiaan Hoogeveen. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See http://www.perl.com/perl/misc/Artistic.html

=cut

1;