The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package AI::Prolog::Parser::PreProcessor::Math;
$REVISION = '$Id: Math.pm,v 1.2 2005/06/20 07:36:48 ovid Exp $';

$VERSION = '0.01';
use strict;
use warnings;
use Regexp::Common;

my $var              = qr/[[:upper:]][[:alnum:]_]*/;
my $num              = $RE{num}{real};
                       # ** must be before *
my $op               = qr{(?:\*\*|[-+*/%])};
my $compare          = qr/(?:(?:\\|=)?=|is|[<>]=?)/;
my $lparen           = qr/\(/;
my $rparen           = qr/\)/;

# Having a word boundary prior to $num breaks the regex
# when trying to match negative numbers
my $simple_math_term = qr/(?!\.(?![0-9]))(?:$num\b|\b$var\b)/;
my $simple_rhs = qr/
    $simple_math_term
    (?:
        \s*
        $op 
        \s*
        $simple_math_term
    )*
/x;
my $simple_group_term = qr/$lparen\s*$simple_rhs\s*$rparen/;
my $math_term = qr/(?:$simple_math_term|$simple_group_term)/;
my $complex_rhs = qr/
    $math_term
    (?:
        \s*
        $op 
        \s*
        $math_term
    )*
/x;
my $complex_group_term = qr/$lparen\s*$complex_rhs\s*$rparen/;
my $final_math_term = qr/(?:$math_term|$complex_group_term)/;
my $rhs = qr/
    $final_math_term
    (?:
        \s*
        $op
        \s*
        $final_math_term
    )*
/x;

my $expression = qr/
    (
        ($simple_math_term)
        \s+
        ($compare)
        \s+
        ($rhs)
    )
    (?=[,.])
/x;

my %convert = (qw{
    is    is
    =     eq
    +     plus
    /     div
    -     minus
    %     mod
    *     mult
    **    pow
    <     lt
    <=    le
    >     gt
    >=    ge
    ==    eq
    \=    ne
});

sub process {
    my ($class, $prolog) = @_;
    while ($prolog =~ $expression) {
        my ($old_expression, $lhs, $comp, $rhs) = ($1, $2, $3, $4);
        my $new_rhs = $class->_parse($class->_lex($rhs));
        my $new_expression = sprintf "%s(%s, %s)" =>
            $convert{$comp},
            $lhs,
            $new_rhs;
        $prolog =~ s/\Q$old_expression\E/$new_expression/g;
    }
    return $prolog;
}

sub _lex {
    my ($class, $rhs) = @_;
    my $lexer = _lexer($rhs);
    my @tokens;
    while (my $token = $lexer->()) {
        push @tokens => $token;
    }
    return \@tokens;
}

sub _lexer {
    my $rhs = shift;
    # the entire "$prev_op" thing is to allow the lexer to be aware of '7 + -3'
    # $op_ok is false on the first pass because it can never be first, but we
    # might have '-7 * (-2 + 3)'
    my $op_ok = 0;
    return sub {
        LEXER: {
            $op_ok = 0, return ['OP',      $1] if $op_ok && $rhs =~ /\G ($op)               /gcx;
            $op_ok = 1, return ['ATOM',    $1] if           $rhs =~ /\G ($simple_math_term) /gcx;
            $op_ok = 0, return ['LPAREN', '('] if           $rhs =~ /\G $lparen             /gcx;
            $op_ok = 1, return ['RPAREN', ')'] if           $rhs =~ /\G $rparen             /gcx;
            redo LEXER                         if           $rhs =~ /\G \s+                 /gcx;
        }
    };
}

sub _parse {
    my ($class, $tokens) = @_;
    my $parens_left = 1;
    REDUCE: while ($parens_left) {
        my ($first, $last);
        for my $i ( 0 .. $#$tokens ) {
            my $token = $tokens->[$i];
            next unless $token;
            if ("(" eq _as_string($token)) {
                $first = $i;
            }
            if (")" eq _as_string($token)) {
                unless (defined $first) {
                    require Carp;
                    # XXX I should probably cache the string and show it.
                    # XXX But it doesn't matter because that shouldn't happen here
                    Carp::croak "Parse error in math pre-processor.  Mismatched parens";
                }
                $last = $i;
                $tokens->[$first] = $class->_parse_group([@{$tokens}[$first + 1 .. $last - 1]]);
                undef $tokens->[$_] for $first + 1 .. $last;
                @$tokens = grep $_ => @$tokens;
                undef $first;
                undef $last;
                redo REDUCE;
            }
        }
        $parens_left = 0 unless defined $first;
    }
    return _as_string($class->_parse_group($tokens));
}

sub _parse_group {
    my ($class, $tokens) = @_;
    foreach my $op_re (qr{(?:\*\*|[*/])}, qr{[+-]}, qr/\%/) {
        for my $i ( 0 .. $#$tokens) {
            my $token = $tokens->[$i];
            if (ref $token && "@$token" =~ /OP ($op_re)/) {
                my $curr_op = $1;
                my $prev = _prev_token($tokens, $i);
                my $next = _next_token($tokens, $i);
                $tokens->[$i] = sprintf "%s(%s, %s)" =>
                    $convert{$curr_op}, 
                    _as_string($tokens->[$prev]),
                    _as_string($tokens->[$next]);
                undef $tokens->[$prev];
                undef $tokens->[$next];
            }
        }
        @$tokens = grep $_ => @$tokens;
    }
    #main::diag Dumper $tokens;
    return $tokens->[0]; # should never have more than on token left
}

sub _prev_token {
    my ($tokens, $index) = @_;
    for my $i ( reverse 0 .. $index - 1) {
        return $i if defined $tokens->[$i];
    }
}

sub _next_token {
    my ($tokens, $index) = @_;
    for my $i ( $index + 1 .. $#$tokens ) {
        return $i if defined $tokens->[$i];
    }
}

sub _as_string { ref $_[0]? $_[0][1] : $_[0] }

sub match { shift; shift =~ $expression }

# The following are testing hooks

sub _compare            { shift; shift =~ /^$compare$/            }
sub _op                 { shift; shift =~ /^$op$/                 }
sub _simple_rhs         { shift; shift =~ /^$simple_rhs$/         }
sub _simple_group_term  { shift; shift =~ /^$simple_group_term$/  }
sub _simple_math_term   { shift; shift =~ /^$simple_math_term$/   }
sub _math_term          { shift; shift =~ /^$math_term$/          }
sub _complex_rhs        { shift; shift =~ /^$complex_rhs$/        }
sub _complex_group_term { shift; shift =~ /^$complex_group_term$/ }

1;

__END__

=head1 NAME

AI::Prolog::Parser::PreProcessor::Math - The AI::Prolog math macro

=head1 SYNOPSIS

 my $program = AI::Prolog::Parser::PreProcessor::Math->process($prolog_text).

=head1 DESCRIPTION

This code reads in the Prolog text and rewrites it to a for that is suitable
for the L<AI::Prolog::Parser|AI::Prolog::Parser> to read.  Users of
L<AI::Prolog||AI::Prolog> should never need to know about this.

=head1 TODO

Constant folding for performance improvment.  No need to internally have
C<is(X, plus(3, 4))> when I can do C<is(X, 5)>.  It shouldn't be too hard.

Figure out how to preserve line number.

=head1 AUTHOR

Curtis "Ovid" Poe, E<lt>moc tod oohay ta eop_divo_sitrucE<gt>

Reverse the name to email me.

=head1 COPYRIGHT AND LICENSE

Copyright 2005 by Curtis "Ovid" Poe

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut