Originally posted on perlmonks.org 2002/09/27
Evaluate user supplied expressions.
Examples:
a = ( 4+(b=2) * (5.2) ) # Sets a to 14.4 and b to 2 returns 14.4 for the result of the entire expression.
a # returns 14.4
a= e || b # sets a to 2 if e undefined
a=b=c=d = int(10.5431) # Set a,b,c,d to 10
Done as an exercise into parsing and executing a mini-language
#!/usr/bin/perl
use warnings;
use strict;
# Operators currently supported.
# %Operators does double duty.
# It includes functions ie: int() and ops ie: a + b
# The difference being that an op takes the form of VAR1 OP VAR2
# and functions take the form of OP (EXPRESSION)
# You can add abritrary ops as long as the don't match a-zA-Z
# Functions can contain letters and numbers.
my $last_value = undef;
my %Operators = (
'+' => sub { @_ = getsymbolval(@_); my $n = 0; $n += $_ foreach @_; $n },
'-' =>
sub { @_ = getsymbolval(@_); my $n = shift; $n -= $_ foreach @_; $n },
'*' =>
sub { @_ = getsymbolval(@_); my $n = shift; $n *= $_ foreach @_; $n },
'/' =>
sub { @_ = getsymbolval(@_); my $n = shift; $n /= $_ foreach @_; $n },
'^' => sub { @_ = getsymbolval(@_); my $n = shift; $n = $n**shift; $n; },
'=' => sub {
no strict;
my ( $n, $v ) = @_;
if ( $n !~ /^[a-zA-Z_]/ ) {
warn "Attempt to assign to read-only variable.\n";
return;
}
${ 'Expression::Evaluate::' . $n } = getsymbolval($v);
},
'==' => sub {
no strict;
my ( $n, $v ) = @_;
getsymbolval($n) == getsymbolval($v);
},
'<' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) < getsymbolval($v);
},
'<=' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) <= getsymbolval($v);
},
'>' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) > getsymbolval($v);
},
'>=' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) >= getsymbolval($v);
},
'&&' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) && getsymbolval($v);
},
'||' => sub {
no strict;
my ( $n, $v ) = @_;
return getsymbolval($n) || getsymbolval($v);
},
'_' => sub { # Concat
no strict;
my ( $n, $v ) = getsymbolval(@_);
$n =~ s/['"]$//;
$v =~ s/^['"]//; #"
return $n . $v;
},
# Functions
'int' => sub { return int getsymbolval(shift) },
'lc' => sub { return lc getsymbolval(shift) },
'uc' => sub { return uc getsymbolval(shift) },
);
# Symbols are for predifined values...
my %symbols = (
':date' => sub { scalar localtime },
':id' => "lee_test",
':last' => sub { return $last_value },
);
{
print "Enter an expession..\n";
my $exp = ;
chomp($exp);
last if $exp =~ /^quit$/;
my $result = parse_expression($exp);
$result = '' unless $result; # Silence warning for undefined.
print "Result: $result\n";
redo;
}
# Subs ########################################
sub parse_expression {
my $exp = shift;
my @tokens = ();
# Strip out invalid ASCII
$exp =~ s/([^\n\r\x20-\x7f])/ /g;
# Pad out ops with spaces.
my $opsregex = join( "", grep { !m/[a-zA-Z]/ } keys %Operators );
$opsregex =~ tr///cs;
$opsregex = quotemeta $opsregex;
$exp =~ s/\s*([$opsregex]+)\s*/ $1 /go;
$exp =~ s/\s*([()])\s*/ $1 /go;
# Get tokens
push @tokens, $1
while $exp =~ /\G\s*(".*?")/gc
or $exp =~ /\G\s*('.*?')/gc
or $exp =~ /\G\s*(\S+)/gc;
if ( @tokens == 1 && $tokens[0] =~ /^[:\w]?\w+$/ ) {
no strict;
return getsymbolval( $tokens[0] );
}
# Find any parens.
my ( @lp, @rp ) = ();
for ( my $p = 0 ; $p < @tokens ; $p++ ) {
if ( $tokens[$p] eq '(' ) {
push @lp, $p;
}
elsif ( $tokens[$p] eq ')' ) {
push @rp, $p;
}
}
if ( @lp != @rp ) {
warn "Mismatched parens in expression.\n";
$last_value = undef;
return;
}
my @temp = @tokens;
for ( my $i = 0 ; $i < @rp ; $i++ ) {
# Find the match in @lp that is < than.
my @wanted;
for ( my $j = $#lp ; $j >= 0 ; $j-- ) {
if ( defined $lp[$j] && $lp[$j] < $rp[$i] ) {
( undef, @wanted ) = @tokens[ $lp[$j] .. ( $rp[$i] - 1 ) ];
# Rewrite "functions"
if ( exists $Operators{ $tokens[ $lp[$j] - 1 ] }
&& $tokens[ $lp[$j] - 1 ] =~ /[a-zA-Z]/ )
{
@wanted = ( $tokens[ $lp[$j] - 1 ], [@wanted] );
$tokens[ $lp[$j] - 1 ] = undef;
}
@tokens[ $lp[$j] .. ( $rp[$i] ) ] = \@wanted;
push @temp, @wanted;
$lp[$j] = $rp[$i] = undef;
last;
}
}
}
my $result = evaluate( \@tokens );
if ( ref $result eq 'ARRAY' ) {
if ( @$result == 0 ) {
$last_value = undef;
return;
}
else { # It's a list
return "(" . join( ", ", @$result ) . ")";
}
}
$last_value = $result;
return $result;
}
#################################################
sub evaluate {
my $ops = shift;
@$ops = grep { defined $_ } @$ops;
foreach my $op (@$ops) {
if ( ref $op eq 'ARRAY' ) {
$op = evaluate($op);
}
}
# Process tokens right to left so assign propagates. (a = b = c = 3)
my %pops = ();
for ( my $i = $#{$ops} ; $i >= 0 ; $i-- ) {
push @{ $pops{ $ops->[$i] } }, $i
if defined $ops->[$i] && exists $Operators{ $ops->[$i] };
}
# Order by precedence.
my @ordered =
map { @{ $pops{$_} } }
grep { defined $pops{$_} } qw( ^ * / && || + - > >= < <= _ == = ), ',';
while ( my $i = shift @ordered ) {
my $op = [ @$ops[ $i, $i - 1, $i + 1 ] ];
splice @{$ops}, $i - 1, 3, $op;
@ordered = map { $_ > $i ? $_ - 2 : $_ } @ordered;
}
my $operator = shift @$ops;
$operator = evaluate($operator) if ref $operator eq 'ARRAY';
if ( defined $operator ) {
if ( defined $Operators{$operator} ) {
$ops = $Operators{$operator}->(@$ops);
}
elsif ( $operator && @$ops ) {
warn "Invalid expressions\n";
warn "$operator:\n";
return;
}
else {
return $operator;
}
}
return $ops;
}
#################################################
sub getsymbolval {
no strict;
my @syms = @_;
foreach my $symbol (@syms) {
next unless defined $symbol;
if ( $symbol && exists $symbols{$symbol} ) {
$symbol =
ref $symbols{$symbol} eq 'CODE'
? $symbols{$symbol}->()
: $symbols{$symbol};
}
elsif ( $symbol =~ /^\D+$/ ) {
unless ( $symbol =~ /^[\"\'].*[\"\']$/ )
{ #" comment to fix syntax highlighting in my editor
$symbol = ${ 'Expression::Evaluate::' . $symbol } || undef;
}
}
else {
}
}
wantarray ? @syms : $syms[0];
}