package CGI::Ex::Var;

=head1 NAME

CGI::Ex::Var - Variable and expression parsing (exprimental)

=head1 DESCRIPTION

Experimental - The storage structure will change to match CGI::Ex::Template by the next release.

=cut

###----------------------------------------------------------------###
#  Copyright 2006 - Paul Seamons                                     #
#  Distributed under the Perl Artistic License without warranty      #
###----------------------------------------------------------------###

use strict;

use vars qw(
            $SCALAR_OPS
            $FILTER_OPS
            $LIST_OPS
            $HASH_OPS
            $FILTERS

            $OPERATORS
            $OP_UNARY
            $OP_BINARY
            $OP_TRINARY

            $QR_OP
            $QR_OP_UNARY
            $QR_OP_PARENED
            $QR_COMMENTS
            $QR_AQ_NOTDOT
            $QR_PRIVATE

            $RT_NAMESPACE
            $RT_FILTERS
            $RT_CONTEXT_SUB
            $RT_DEBUG_UNDEF
            $RT_UNDEFINED_SUB
            $RT_OPERATOR_PRECEDENCE
            $RT_DURING_COMPILE

            $TT_FILTERS
            );
use constant trace => 0;

BEGIN {
    $SCALAR_OPS = {
        chunk    => \&vmethod_chunk,
        collapse => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; s/\s+/ /g; $_ },
        defined  => sub { 1 },
        indent   => \&vmethod_indent,
        'format' => \&vmethod_format,
        hash     => sub { {value => $_[0]} },
        html     => sub { local $_ = $_[0]; s/&/&amp;/g; s/</&lt;/g; s/>/&gt;/g; s/\"/&quot;/g; $_ },
        lcfirst  => sub { lcfirst $_[0] },
        length   => sub { defined($_[0]) ? length($_[0]) : 0 },
        lower    => sub { lc $_[0] },
        match    => \&vmethod_match,
        null     => sub { '' },
        remove   => sub { vmethod_replace(shift, shift, '', 1) },
        repeat   => \&vmethod_repeat,
        replace  => \&vmethod_replace,
        search   => sub { my ($str, $pat) = @_; return $str if ! defined $str || ! defined $pat; return $str =~ /$pat/ },
        size     => sub { 1 },
        split    => \&vmethod_split,
        stderr   => sub { print STDERR $_[0]; '' },
        substr   => sub { my ($str, $i, $len) = @_; defined($len) ? substr($str, $i, $len) : substr($str, $i) },
        trim     => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; $_ },
        ucfirst  => sub { ucfirst $_[0] },
        upper    => sub { uc $_[0] },
        uri      => sub { local $_ = $_[0]; s/([^;\/?:@&=+\$,A-Za-z0-9\-_.!~*\'()])/sprintf('%%%02X', ord($1))/eg; $_ },
    };

    $FILTER_OPS = { # generally - non-dynamic filters belong in scalar ops
        eval     => [\&filter_eval, 1],
        evaltt   => [\&filter_eval, 1],
        file     => [\&filter_redirect, 1],
        redirect => [\&filter_redirect, 1],
    };

    $LIST_OPS = {
        first   => sub { my ($ref, $i) = @_; return $ref->[0] if ! $i; return [@{$ref}[0 .. $i - 1]]},
        grep    => sub { my ($ref, $pat) = @_; [grep {/$pat/} @$ref] },
        hash    => sub { my ($list, $i) = @_; defined($i) ? {map {$i++ => $_} @$list} : {@$list} },
        join    => sub { my ($ref, $join) = @_; $join = ' ' if ! defined $join; local $^W; return join $join, @$ref },
        last    => sub { my ($ref, $i) = @_; return $ref->[-1] if ! $i; return [@{$ref}[-$i .. -1]]},
        list    => sub { $_[0] },
        max     => sub { $#{ $_[0] } },
        merge   => sub { my $ref = shift; return [ @$ref, grep {defined} map {ref eq 'ARRAY' ? @$_ : undef} @_ ] },
        nsort   => \&vmethod_nsort,
        pop     => sub { pop @{ $_[0] } },
        push    => sub { my $ref = shift; push @$ref, @_; return '' },
        reverse => sub { [ reverse @{ $_[0] } ] },
        shift   => sub { shift  @{ $_[0] } },
        size    => sub { scalar @{ $_[0] } },
        slice   => sub { my ($ref, $a, $b) = @_; $a ||= 0; $b = $#$ref if ! defined $b; return [@{$ref}[$a .. $b]] },
        sort    => \&vmethod_sort,
        splice  => \&vmethod_splice,
        unique  => sub { my %u; return [ grep { ! $u{$_} ++ } @{ $_[0] } ] },
        unshift => sub { my $ref = shift; unshift @$ref, @_; return '' },
    };

    $HASH_OPS = {
        defined => sub { return '' if ! defined $_[1]; defined $_[0]->{ $_[1] } },
        delete  => sub { return '' if ! defined $_[1]; delete  $_[0]->{ $_[1] } },
        each    => sub { [%{ $_[0] }] },
        exists  => sub { return '' if ! defined $_[1]; exists $_[0]->{ $_[1] } },
        hash    => sub { $_[0] },
        import  => sub { my ($a, $b) = @_; return '' if ref($b) ne 'HASH'; @{$a}{keys %$b} = values %$b; '' },
        keys    => sub { [keys %{ $_[0] }] },
        list    => sub { [$_[0]] },
        pairs   => sub { [map { {key => $_, value => $_[0]->{$_}} } keys %{ $_[0] } ] },
        nsort   => sub { my $ref = shift; [sort {$ref->{$a}    <=> $ref->{$b}   } keys %$ref] },
        size    => sub { scalar keys %{ $_[0] } },
        sort    => sub { my $ref = shift; [sort {lc $ref->{$a} cmp lc $ref->{$b}} keys %$ref] },
        values  => sub { [values %{ $_[0] }] },
    };

    ### Runtime set variables that control lookups of various pieces of info
    $RT_NAMESPACE   = {};
    $RT_FILTERS     = {};
    $RT_CONTEXT_SUB = sub { {} };
    $RT_DEBUG_UNDEF = 0;
    $RT_OPERATOR_PRECEDENCE = 0;

    ### setup the operator parsing
    $OPERATORS ||= [
        # name => # order, precedence, symbols, only_in_parens, sub to create
        [2, 96, ['**', '^', 'pow'],  0, sub {bless(shift(), 'CGI::Ex::_pow')}    ],
        [1, 93, ['!'],               0, sub {bless(shift(), 'CGI::Ex::_not')}    ],
        [1, 93, ['-'],               0, sub {bless(shift(), 'CGI::Ex::_negate')} ],
        [2, 90, ['*'],               0, sub {bless(shift(), 'CGI::Ex::_mult')}   ],
        [2, 90, ['/'],               0, sub {bless(shift(), 'CGI::Ex::_div')}    ],
        [2, 90, ['div', 'DIV'],      0, sub {bless(shift(), 'CGI::Ex::_intdiv')} ],
        [2, 90, ['%', 'mod', 'MOD'], 0, sub {bless(shift(), 'CGI::Ex::_mod')}    ],
        [2, 85, ['+'],               0, sub {bless(shift(), 'CGI::Ex::_plus')}   ],
        [2, 85, ['-'],               0, sub {bless(shift(), 'CGI::Ex::_subtr')}  ],
        [2, 85, ['_', '~'],          0, \&_concat                                ],
        [2, 80, ['<'],               0, sub {bless(shift(), 'CGI::Ex::_num_lt')} ],
        [2, 80, ['>'],               0, sub {bless(shift(), 'CGI::Ex::_num_gt')} ],
        [2, 80, ['<='],              0, sub {bless(shift(), 'CGI::Ex::_num_le')} ],
        [2, 80, ['>='],              0, sub {bless(shift(), 'CGI::Ex::_num_ge')} ],
        [2, 80, ['lt'],              0, sub {bless(shift(), 'CGI::Ex::_str_lt')} ],
        [2, 80, ['gt'],              0, sub {bless(shift(), 'CGI::Ex::_str_gt')} ],
        [2, 80, ['le'],              0, sub {bless(shift(), 'CGI::Ex::_str_le')} ],
        [2, 80, ['ge'],              0, sub {bless(shift(), 'CGI::Ex::_str_ge')} ],
        [2, 75, ['==', 'eq'],        0, sub {bless(shift(), 'CGI::Ex::_eq')}     ],
        [2, 75, ['!=', 'ne'],        0, sub {bless(shift(), 'CGI::Ex::_ne')}     ],
        [2, 70, ['&&'],              0, sub {bless(shift(), 'CGI::Ex::_and')}    ],
        [2, 65, ['||'],              0, sub {bless(shift(), 'CGI::Ex::_or')}     ],
        [2, 60, ['..'],              0, sub {bless(shift(), 'CGI::Ex::_range')}  ],
        [3, 55, ['?', ':'],          0, sub {bless(shift(), 'CGI::Ex::_ifelse')} ],
        [2, 52, ['='],               1, sub {bless(shift(), 'CGI::Ex::_set')}    ],
        [1, 50, ['not', 'NOT'],      0, sub {bless(shift(), 'CGI::Ex::_not')}    ],
        [2, 45, ['and', 'AND'],      0, sub {bless(shift(), 'CGI::Ex::_and')}    ],
        [2, 40, ['or', 'OR'],        0, sub {bless(shift(), 'CGI::Ex::_or')}     ],
    ];

    $OP_UNARY   ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 1} @$OPERATORS};
    $OP_BINARY  ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 2} @$OPERATORS};
    $OP_TRINARY ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 3} @$OPERATORS};
    sub _op_qr { # no mixed \w\W operators
        my %used;
        my $chrs = join '|', map {quotemeta $_} grep {++$used{$_} < 2} grep {/^\W{2,}$/} @_;
        my $chr  = join '',  map {quotemeta $_} grep {++$used{$_} < 2} grep {/^\W$/}     @_;
        my $word = join '|',                    grep {++$used{$_} < 2} grep {/^\w+$/}    @_;
        $chr = "[$chr]" if $chr;
        $word = "\\b(?:$word)\\b" if $word;
        return join('|', grep {length} $chrs, $chr, $word) || die "Missing operator regex";
    }
    sub _build_op_qr       { _op_qr(sort map {@{ $_->[2] }} grep {$_->[0] > 1 && ! $_->[3]} @$OPERATORS) } # all binary, trinary, non-parened ops
    sub _build_op_qr_unary { _op_qr(sort map {@{ $_->[2] }} grep {$_->[0] == 1            } @$OPERATORS) } # unary operators
    sub _build_op_qr_paren { _op_qr(sort map {@{ $_->[2] }} grep {                 $_->[3]} @$OPERATORS) } # paren
    $QR_OP         ||= _build_op_qr();
    $QR_OP_UNARY   ||= _build_op_qr_unary();
    $QR_OP_PARENED ||= _build_op_qr_paren();

    $QR_COMMENTS   = '(?-s: \# .* \s*)*';
    $QR_AQ_NOTDOT  = "(?! \\s* $QR_COMMENTS \\.)";
    $QR_PRIVATE    = qr/^_/;
};

###----------------------------------------------------------------###

sub _var     { return bless(shift(), __PACKAGE__        ) }
sub _literal { return bless(shift(), 'CGI::Ex::_literal') }
sub _hash    { return bless(shift(), 'CGI::Ex::_hash'   ) }
sub _array   { return bless(shift(), 'CGI::Ex::_array'  ) }
sub _concat  { return bless(shift(), 'CGI::Ex::_concat' ) }
sub _autobox { return bless(shift(), 'CGI::Ex::_autobox') }
sub _not     { return bless(shift(), 'CGI::Ex::_not'    ) }

sub throw {
    require CGI::Ex::Template;
    CGI::Ex::Template->throw(@_);
}

###----------------------------------------------------------------###

sub parse_exp {
    my $str_ref = shift;
    my $ARGS    = shift || {};

    ### allow for custom auto_quoting (such as hash constructors)
    if ($ARGS->{'auto_quote'}) {
        if ($$str_ref =~ $ARGS->{'auto_quote'}) {
            my $str = $1;
            substr($$str_ref, 0, length($str), '');
            $$str_ref =~ s{ ^ \s* $QR_COMMENTS }{}ox;
            return $str;
        ### allow for auto-quoted $foo or ${foo.bar} type constructs
        } elsif ($$str_ref =~ s{ ^ \$ (\w+ (?:\.\w+)*) \b \s* $QR_COMMENTS }{}ox
                 || $$str_ref =~ s{ ^ \$\{ \s* ([^\}]+) \} \s* $QR_COMMENTS }{}ox) {
            my $name = $1;
            return parse_exp(\$name);
        }
    }

    my $copy = $$str_ref; # copy while parsing to allow for errors

    ### test for leading unary operators
    my $has_unary;
    if ($copy =~ s{ ^ ($QR_OP_UNARY) \s* $QR_COMMENTS }{}ox) {
        return if $ARGS->{'auto_quote'}; # auto_quoted thing was too complicated
        $has_unary = $1;
    }

    my @var;
    my $is_literal;
    my $is_construct;
    my $is_namespace;

    ### allow for numbers
    if ($copy =~ s{ ^ ( (?:\d*\.\d+ | \d+) ) \s* $QR_COMMENTS }{}ox) {
        my $number = $1;
        push @var, _literal(\ $number);
        $is_literal = 1;

    ### looks like a normal variable start
    } elsif ($copy =~ s{ ^ (\w+) \s* $QR_COMMENTS }{}ox) {
        push @var, $1;
        $is_namespace = 1 if $RT_NAMESPACE->{$1};

    ### allow for literal strings
    } elsif ($copy =~ s{ ^ ([\"\']) (|.*?[^\\]) \1 \s* $QR_COMMENTS }{}sox) {
        if ($1 eq "'") { # no interpolation on single quoted strings
            my $str = $2;
            $str =~ s{ \\\' }{\'}xg;
            push @var, _literal(\ $str);
            $is_literal = 1;
        } else {
            my $str = $2;
            $str =~ s/\\n/\n/g;
            $str =~ s/\\t/\t/g;
            $str =~ s/\\r/\r/g;
            $str =~ s/\\([\"\$])/$1/g;
            my @pieces = $ARGS->{'auto_quote'}
                ? split(m{ (\$\w+            | \$\{ [^\}]+ \}) }x, $str)  # autoquoted items get a single $\w+ - no nesting
                : split(m{ (\$\w+ (?:\.\w+)* | \$\{ [^\}]+ \}) }x, $str);
            my $n = 0;
            foreach my $piece (@pieces) {
                next if ! ($n++ % 2);
                next if $piece !~ m{ ^ \$ (\w+ (?:\.\w+)*) $ }x
                    && $piece !~ m{ ^ \$\{ \s* ([^\}]+) \} $ }x;
                my $name = $1;
                $piece = parse_exp(\$name);
            }
            @pieces = grep {defined && length} @pieces;
            if (@pieces == 1 && ! ref $pieces[0]) {
                push @var, _literal(\ $pieces[0]);
                $is_literal = 1;
            } elsif (! @pieces) {
                my $str = '';
                push @var, _literal(\ $str);
                $is_literal = 1;
            } else {
                push @var, _concat(\@pieces);
                $is_construct = 1;
            }
        }
        if ($ARGS->{'auto_quote'}){
            $$str_ref = $copy;
            return ${ $var[0] } if $is_literal;
            return _var([@var, 0]);
        }

    ### allow for leading $foo or ${foo.bar} type constructs
    } elsif ($copy =~ s{ ^ \$ (\w+) \b \s* $QR_COMMENTS }{}ox
        || $copy =~ s{ ^ \$\{ \s* ([^\}]+) \} \s* $QR_COMMENTS }{}ox) {
        my $name = $1;
        push @var, parse_exp(\$name);

    ### looks like an array constructor
    } elsif ($copy =~ s{ ^ \[ \s* $QR_COMMENTS }{}ox) {
        local $RT_OPERATOR_PRECEDENCE = 0; # reset presedence
        my $arrayref = [];
        while (defined(my $var = parse_exp(\$copy))) {
            push @$arrayref, $var;
            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
        }
        $copy =~ s{ ^ \] \s* $QR_COMMENTS }{}ox
            || throw('parse.missing.square', "Missing close \]", undef, length($$str_ref) - length($copy));
        push @var, _array($arrayref);
        $is_construct = 1;

    ### looks like a hash constructor
    } elsif ($copy =~ s{ ^ \{ \s* $QR_COMMENTS }{}ox) {
        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
        my $hashref = [];
        while (defined(my $key = parse_exp(\$copy, {auto_quote => qr{ ^ (\w+) $QR_AQ_NOTDOT }xo}))) {
            $copy =~ s{ ^ = >? \s* $QR_COMMENTS }{}ox;
            my $val = parse_exp(\$copy);
            push @$hashref, $key, $val;
            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
        }
        $copy =~ s{ ^ \} \s* $QR_COMMENTS }{}ox
            || throw('parse.missing.curly', "Missing close \} ($copy)", undef, length($$str_ref) - length($copy));
        push @var, _hash($hashref);
        $is_construct = 1;

    ### looks like a paren grouper
    } elsif ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
        my $var = parse_exp(\$copy, {allow_parened_ops => 1});
        $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
            || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
        push @var, $var;
        $is_construct = 1;

    ### nothing to find - return failure
    } else {
        return;
    }

    return if $ARGS->{'auto_quote'}; # auto_quoted thing was too complicated

    ### looks for args for the initial
    if ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
        my $args = parse_args(\$copy);
        $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
            || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
        push @var, $args;
    } else {
        push @var, 0;
    }

    ### allow for nested items
    while ($copy =~ s{ ^ ( \.(?!\.) | \|(?!\|) ) \s* $QR_COMMENTS }{}ox) {
        push(@var, $1) if ! $ARGS->{'no_dots'};

        ### allow for interpolated variables in the middle - one.$foo.two or one.${foo.bar}.two
        if ($copy =~ s{ ^ \$(\w+) \s* $QR_COMMENTS }{}ox
            || $copy =~ s{ ^ \$\{ \s* ([^\}]+)\} \s* $QR_COMMENTS }{}ox) {
            my $name = $1;
            my $var = parse_exp(\$name);
            push @var, $var;
        } elsif ($copy =~ s{ ^ (\w+) \s* $QR_COMMENTS }{}ox) {
            push @var, $1;
        } else {
            throw('parse', "Not sure how to continue parsing on \"$copy\" ($$str_ref)");
        }

        ### looks for args for the nested item
        if ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
            local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
            my $args = parse_args(\$copy);
            $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
                || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
            push @var, $args;
        } else {
            push @var, 0;
        }

    }

    ### flatten literals and constants as much as possible
    my $var;
    if (@var == 2) {
        if ($is_literal) {
            $var = ${ $var[0] };
        } elsif ($is_construct) {
            $var = $var[0];
        } else {
            $var = _var(\@var);
        }
    } else {
        if ($is_construct && ! $var[0]->does_autobox) {
            $var[0] = _autobox([$var[0]]);
        }

        if ($is_namespace) { # attempt to "fold" constant variables into the parse tree
            local $RT_DURING_COMPILE = 1;
            $var = _var(\@var)->call({});
        } else {
            $var = _var(\@var);
        }
    }

    ### allow for all "operators"
    if (! $RT_OPERATOR_PRECEDENCE) {
        my $tree;
        my $found;
        while ($copy =~ s{ ^ ($QR_OP) \s* $QR_COMMENTS }{}ox ## look for operators - then move along
               || ($ARGS->{'allow_parened_ops'}
                   && $copy =~ s{ ^ ($QR_OP_PARENED) \s* $QR_COMMENTS }{}ox) ) {
            local $RT_OPERATOR_PRECEDENCE = 1;
            my $op   = $1;
            my $var2 = parse_exp(\$copy);

            ### allow for unary operator precedence
            if ($has_unary && (($OP_BINARY->{$op} || $OP_TRINARY->{$op})->[1] < $OP_UNARY->{$has_unary}->[1])) {
                if ($tree) {
                    if (@$tree == 2) { # only one operator - keep simple things fast
                        $var = $OP_BINARY->{$tree->[0]}->[4]->([$var, $tree->[1]]);
                    } else {
                        unshift @$tree, $var;
                        $var = apply_precedence($tree, $found);
                    }
                    undef $tree;
                    undef $found;
                }
                $var = $OP_UNARY->{$has_unary}->[4]->([$var]);
                undef $has_unary;
            }

            ### add the operator to the tree
            push (@{ $tree ||= [] }, $op, $var2);
            my $ref = $OP_BINARY->{$op} || $OP_TRINARY->{$op};
            $found->{$op} = $ref->[1];
        }

        ### if we found operators - tree the nodes by operator precedence
        if ($tree) {
            if (@$tree == 2 && $OP_BINARY->{$tree->[0]}) { # only one operator - keep simple things fast
                $var = $OP_BINARY->{$tree->[0]}->[4]->([$var, $tree->[1]]);
            } else {
                unshift @$tree, $var;
                $var = apply_precedence($tree, $found);
            }
        }
    }

    ### allow for unary on non-chained variables
    if ($has_unary) {
        $var = $OP_UNARY->{$has_unary}->[4]->([$var]);
    }

    $$str_ref = $copy; # commit the changes
    return $var;
}

### this is used to put the parsed variables into the correct operations tree
sub apply_precedence {
    my ($tree, $found) = @_;

    my @var;
    my $trees;
    ### look at the operators we found in the order we found them
    for my $op (sort {$found->{$a} <=> $found->{$b}} keys %$found) {
        local $found->{$op};
        delete $found->{$op};
        my @trees;
        my @trinary;

        ### split the array on the current operator
        for (my $i = 0; $i <= $#$tree; $i ++) {
            my $is_trinary = $OP_TRINARY->{$op} && grep {$_ eq $tree->[$i]} @{ $OP_TRINARY->{$op}->[2] };
            next if $tree->[$i] ne $op && ! $is_trinary;
            push @trees, [splice @$tree, 0, $i, ()]; # everything up to the operator
            push @trinary, $tree->[0] if $is_trinary;
            shift @$tree; # pull off the operator
            $i = -1;
        }
        next if ! @trees; # this iteration didn't have the current operator
        push @trees, $tree if scalar @$tree; # elements after last operator

        ### now - for this level split on remaining operators, or add the variable to the tree
        for my $node (@trees) {
            if (@$node == 1) {
                $node = $node->[0]; # single item - its not a tree
            } elsif (@$node == 3) {
                my $ref = $OP_BINARY->{$node->[1]} || $OP_TRINARY->{$node->[1]};
                $node = $ref->[4]->([$node->[0], $node->[2]]); # single operator - put it straight on
            } else {
                $node = apply_precedence($node, $found); # more complicated - recurse
            }
        }

        ### return binary
        if ($OP_BINARY->{$op}) {
            my $val = $trees[0];
            $val = $OP_BINARY->{$op}->[4]->([$val, $trees[$_]]) for 1 .. $#trees;
            return $val;
        }

        ### return simple trinary
        if (@trinary == 2) {
            return $OP_TRINARY->{$op}->[4]->(\@trees);
        }

        ### reorder complex trinary - rare case
        while ($#trinary >= 1) {
            ### if we look starting from the back - the first lead trinary op will always be next to its matching op
            for (my $i = $#trinary; $i >= 0; $i --) {
                next if $OP_TRINARY->{$trinary[$i]}->[2]->[1] eq $trinary[$i];
                my ($op, $op2) = splice @trinary, $i, 2, (); # remove the found pair of operators
                my $node = $OP_TRINARY->{$op}->[4]->([@trees[$i .. $i + 2]]);
                splice @trees, $i, 3, $node; # replace the previous 3 pieces with the one new node
            }
        }
        return $trees[0]; # at this point the trinary has been reduced to a single operator

    }

    throw('parse', "Couldn't apply precedence");
}

### look for arguments - both positional and named
sub parse_args {
    my $str_ref = shift;
    my $ARGS    = shift || {};
    my $copy    = $$str_ref;

    my @args;
    my @named;
    while (length $$str_ref) {
        my $copy = $$str_ref;
        if (defined(my $name = parse_exp(\$copy, {auto_quote => qr{ ^ (\w+) $QR_AQ_NOTDOT }xo}))
            && $copy =~ s{ ^ = >? \s* $QR_COMMENTS }{}ox) {
            throw('parse', 'Named arguments not allowed') if $ARGS->{'positional_only'};
            my $val = parse_exp(\$copy);
            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
            push @named, $name, $val;
            $$str_ref = $copy;
        } elsif (defined(my $arg = parse_exp($str_ref))) {
            push @args, $arg;
            $$str_ref =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
        } else {
            last;
        }
    }

    ### allow for named arguments to be added also
    push @args, _hash(\@named) if scalar @named;

    return \@args;
}

sub get_exp { ref($_[0]) ? $_[0]->call($_[1]) : $_[0] }

sub set_exp {
    my $var = shift;
    $var = _var([$var, 0]) if ! ref $var; # allow for the parse tree to store literals - the literal is used as a name (like [% 'a' = 'A' %])
    return $var->set($_[0], $_[1]);
}


sub dump_parse {
    my $str = shift;
    require Data::Dumper;
    return Data::Dumper::Dumper(parse_exp(\$str));
}

sub dump_get {
    my ($str, $hash) = @_;
    require Data::Dumper;
    return Data::Dumper::Dumper(get_exp(parse_exp(\$str), $hash));
}

sub dump_set {
    my ($str, $val, $hash) = @_;
    $hash ||= {};
    require Data::Dumper;
    set_exp(parse_exp(\$str), $val, $hash);
    return Data::Dumper::Dumper($hash);
}

sub vivify_args {
    my $vars = shift;
    my $hash = shift;
    return [map {get_exp($_, $hash)} @$vars];
}

###----------------------------------------------------------------###

sub new {
    my $class = shift;
    return bless $_[0], $class;
}

sub does_autobox { 0 }

sub call {
    my $self = shift;
    my $hash = shift || {};
    my $i    = 0;

    ### determine the top level of this particular variable access
    my $ref  = $self->[$i++];
    my $args = $self->[$i++];
    warn "CGI::Ex::Var::call: begin \"$ref\"\n" if trace;

    if (ref $ref) {
        if ($ref->does_autobox) {
            $ref = $ref->call($hash);
        } else {
            $ref = $ref->call($hash);
            return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _
            $ref = $hash->{$ref};
        }
    } else {
        if ($RT_DURING_COMPILE) {
            $ref = $RT_NAMESPACE->{$ref};
        } else {
            return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _
            $ref = $hash->{$ref};
        }
    }

    my %seen_filters;
    while (defined $ref) {

        ### check at each point if the returned thing was a code
        if (UNIVERSAL::isa($ref, 'CODE')) {
            my @results = $ref->($args ? (map {get_exp($_, $hash)} @$args) : ());
            if (defined $results[0]) {
                $ref = ($#results > 0) ? \@results : $results[0];
            } elsif (defined $results[1]) {
                die $results[1]; # TT behavior - why not just throw ?
            } else {
                $ref = undef;
                last;
            }
        }

        ### descend one chained level
        last if $i >= $#$self;
        my $was_dot_call = $self->[$i++] eq '.';
        my $name         = $self->[$i++];
        my $args         = $self->[$i++];
        warn "CGI::Ex::Var::get_exp: nested \"$name\"\n" if trace;

        ### allow for named portions of a variable name (foo.$name.bar)
        if (ref $name) {
            $name = $name->call($hash);
            if (! defined $name) {
                $ref = undef;
                last;
            }
        }

        if ($name =~ $QR_PRIVATE) { # don't allow vars that begin with _
            $ref = undef;
            last;
        }

        ### allow for scalar and filter access (this happens for every non virtual method call)
        if (! ref $ref) {
            if ($SCALAR_OPS->{$name}) {                        # normal scalar op
                $ref = $SCALAR_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());

            } elsif ($LIST_OPS->{$name}) {                     # auto-promote to list and use list op
                $ref = $LIST_OPS->{$name}->([$ref], $args ? (map {get_exp($_, $hash)} @$args) : ());

            } elsif (my $filter = $RT_FILTERS->{$name}           # filter configured in Template args
                     || $FILTER_OPS->{$name}                     # predefined filters in CET
                     || (UNIVERSAL::isa($name, 'CODE') && $name) # looks like a filter sub passed in the stash
                     || list_filters()->{$name}) {               # filter defined in Template::Filters

                if (UNIVERSAL::isa($filter, 'CODE')) {
                    $ref = eval { $filter->($ref) }; # non-dynamic filter - no args
                    if (my $err = $@) {
                        throw('filter', $err) if ref($err) !~ /Template::Exception$/;
                        die $err;
                    }
                } elsif (! UNIVERSAL::isa($filter, 'ARRAY')) {
                    throw('filter', "invalid FILTER entry for '$name' (not a CODE ref)");

                } elsif (@$filter == 2 && UNIVERSAL::isa($filter->[0], 'CODE')) { # these are the TT style filters
                    eval {
                        my $sub = $filter->[0];
                        if ($filter->[1]) { # it is a "dynamic filter" that will return a sub
                            ($sub, my $err) = $sub->($RT_CONTEXT_SUB->(), $args ? (map {get_exp($_, $hash)} @$args) : ());
                            if (! $sub && $err) {
                                throw('filter', $err) if ref($err) !~ /Template::Exception$/;
                                die $err;
                            } elsif (! UNIVERSAL::isa($sub, 'CODE')) {
                                throw('filter', "invalid FILTER for '$name' (not a CODE ref)")
                                    if ref($sub) !~ /Template::Exception$/;
                                die $sub;
                            }
                        }
                        $ref = $sub->($ref);
                    };
                    if (my $err = $@) {
                        throw('filter', $err) if ref($err) !~ /Template::Exception$/;
                        die $err;
                    }
                } else { # this looks like our vmethods turned into "filters" (a filter stored under a name)
                    throw('filter', 'Recursive filter alias \"$name\"') if $seen_filters{$name} ++;
                    $self = [$name, 0, '|', @$filter, @{$self}[$i..$#$self]]; # splice the filter into our current tree
                    $i = 2;
                }
                if (scalar keys %seen_filters
                    && $seen_filters{$self->[$i - 5] || ''}) {
                    throw('filter', "invalid FILTER entry for '".$self->[$i - 5]."' (not a CODE ref)");
                }
            } else {
                $ref = undef;
            }

        } else {

            ### method calls on objects
            if (UNIVERSAL::can($ref, 'can')) {
                my @args = $args ? (map {get_exp($_, $hash)} @$args) : ();
                my @results = eval { $ref->$name(@args) };
                if ($@) {
                    die $@ if ref $@ || $@ !~ /Can\'t locate object method/;
                } elsif (defined $results[0]) {
                    $ref = ($#results > 0) ? \@results : $results[0];
                    next;
                } elsif (defined $results[1]) {
                    die $results[1]; # TT behavior - why not just throw ?
                } else {
                    $ref = undef;
                    last;
                }
                # didn't find a method by that name - so fail down to hash and array access
            }

            ### hash member access
            if (UNIVERSAL::isa($ref, 'HASH')) {
                if ($was_dot_call && exists($ref->{$name}) ) {
                    $ref = $ref->{$name};
                } elsif ($HASH_OPS->{$name}) {
                    $ref = $HASH_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());
                } elsif ($RT_DURING_COMPILE) {
                    return $self; # abort - can't fold namespace variable
                } else {
                    $ref = undef;
                }

            ### array access
            } elsif (UNIVERSAL::isa($ref, 'ARRAY')) {
                if ($name =~ /^\d+$/) {
                    $ref = ($name > $#$ref) ? undef : $ref->[$name];
                } else {
                    $ref = (! $LIST_OPS->{$name}) ? undef : $LIST_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());
                }
            }
        }

    } # end of while

    ### allow for undefinedness
    if (! defined $ref) {
        if ($RT_DEBUG_UNDEF) {
            my $chunk = $self->[$i - 2];
            $chunk = $chunk->call($hash) if ref $chunk;
            die "$chunk is undefined\n";
        } else {
            $ref = $self->undefined_any($self);
        }
    }

    return $ref;
}

sub undefined_any { $RT_UNDEFINED_SUB ? $RT_UNDEFINED_SUB->(@_) : undef }

sub set {
    my ($self, $val, $hash) = @_;
    my $i = 0;

    ### determine the top level of this particular variable access
    my $ref  = $self->[$i++];
    my $args = $self->[$i++];

    if (ref $ref) {
        $ref = $ref->call($hash);
        return if ! defined $ref;
    }

    return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _

    if ($#$self <= $i) {
        $hash->{$ref} = $val;
        return;
    } else {
        $ref = $hash->{$ref} ||= {};
    }

    ### let the top level thing be a code block
    return if UNIVERSAL::isa($ref, 'CODE');

    ### vivify the chained levels
    while (defined $ref && $#$self > $i) {
        my $was_dot_call = $self->[$i++] eq '.';
        my $name         = $self->[$i++];
        my $args         = $self->[$i++];

        ### allow for named portions of a variable name (foo.$name.bar)
        if (ref $name) {
            $name = $name->call($hash);
            if (! defined $name) {
                $ref = undef;
                last;
            }
        }

        if ($name =~ $QR_PRIVATE) { # don't allow vars that begin with _
            return;
        }

        ### method calls on objects
        if (UNIVERSAL::can($ref, 'can')) {
            my $lvalueish;
            my @args = $args ? (map {get_exp($_, $hash)} @$args) : ();
            if ($i >= $#$self) {
                $lvalueish = 1;
                push @args, $val;
            }
            my @results = eval { $ref->$name(@args) };
            if ($@) {
                die $@ if ref $@ || $@ !~ /Can\'t locate object method/;
            } elsif (defined $results[0]) {
                $ref = ($#results > 0) ? \@results : $results[0];
            } elsif (defined $results[1]) {
                die $results[1]; # TT behavior - why not just throw ?
            } else {
                $ref = undef;
            }
            return if $lvalueish;
            next;
        }

        ### hash member access
        if (UNIVERSAL::isa($ref, 'HASH')) {
            if ($#$self <= $i) {
                $ref->{$name} = $val;
                return;
            } else {
                $ref = $ref->{$name} ||= {};
                next;
            }

        ### array access
        } elsif (UNIVERSAL::isa($ref, 'ARRAY')) {
            if ($name =~ /^\d+$/) {
                if ($#$self <= $i) {
                    $ref->[$name] = $val;
                    return;
                } else {
                    $ref = $ref->[$name] ||= {};
                    next;
                }
            } else {
                return;
            }

        ### scalar access
        } elsif (! ref($ref) && defined($ref)) {
            return;
        }

        ### check at each point if the returned thing was a code
        if (defined($ref) && UNIVERSAL::isa($ref, 'CODE')) {
            my @results = $ref->($args ? (map {get_exp($_, $hash)} @$args) : ());
            if (defined $results[0]) {
                $ref = ($#results > 0) ? \@results : $results[0];
            } elsif (defined $results[1]) {
                die $results[1]; # TT behavior - why not just throw ?
            } else {
                return;
            }
        }

    }

    return $ref;
}

###----------------------------------------------------------------###
### filters and vmethod definition

sub list_filters {
    return $TT_FILTERS ||= eval { require Template::Filters; $Template::Filters::FILTERS } || {};
}

sub vmethod_chunk {
    my $str  = shift;
    my $size = shift || 1;
    my @list;
    if ($size < 0) { # chunk from the opposite end
        $str = reverse $str;
        $size = -$size;
        unshift(@list, scalar reverse $1) while $str =~ /( .{$size} | .+ )/xg;
    } else {
        push(@list, $1)                   while $str =~ /( .{$size} | .+ )/xg;
    }
    return \@list;
}

sub vmethod_indent {
    my $str = shift; $str = '' if ! defined $str;
    my $pre = shift; $pre = 4  if ! defined $pre;
    $pre = ' ' x $pre if $pre =~ /^\d+$/;
    $str =~ s/^/$pre/mg;
    return $str;
}

sub vmethod_format {
    my $str = shift; $str = ''   if ! defined $str;
    my $pat = shift; $pat = '%s' if ! defined $pat;
    return join "\n", map{ sprintf $pat, $_ } split(/\n/, $str);
}

sub vmethod_match {
    my ($str, $pat, $global) = @_;
    return [] if ! defined $str || ! defined $pat;
    my @res = $global ? ($str =~ /$pat/g) : ($str =~ /$pat/);
    return (@res >= 2) ? \@res : (@res == 1) ? $res[0] : '';
}

sub vmethod_nsort {
    my ($list, $field) = @_;
    return defined($field)
        ? [map {$_->[0]} sort {$a->[1] <=> $b->[1]} map {[$_, (ref $_ eq 'HASH' ? $_->{$field}
                                                               : UNIVERSAL::can($_, $field) ? $_->$field()
                                                               : $_)]} @$list ]
        : [sort {$a <=> $b} @$list];
}

sub vmethod_repeat {
    my ($str, $n, $join) = @_;
    return if ! length $str;
    $n = 1 if ! defined($n) || ! length $n;
    $join = '' if ! defined $join;
    return join $join, ($str) x $n;
}

### This method is a combination of my submissions along
### with work from Andy Wardley, Sergey Martynoff, Nik Clayton, and Josh Rosenbaum
sub vmethod_replace {
    my ($text, $pattern, $replace, $global) = @_;
    $text      = '' unless defined $text;
    $pattern   = '' unless defined $pattern;
    $replace   = '' unless defined $replace;
    $global    = 1  unless defined $global;
    my $expand = sub {
        my ($chunk, $start, $end) = @_;
        $chunk =~ s{ \\(\\|\$) | \$ (\d+) }{
            $1 ? $1
                : ($2 > $#$start || $2 == 0) ? ''
                : substr($text, $start->[$2], $end->[$2] - $start->[$2]);
        }exg;
        $chunk;
    };
    if ($global) {
        $text =~ s{$pattern}{ $expand->($replace, [@-], [@+]) }eg;
    } else {
        $text =~ s{$pattern}{ $expand->($replace, [@-], [@+]) }e;
    }
    return $text;
}

sub vmethod_sort {
    my ($list, $field) = @_;
    return defined($field)
        ? [map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, lc(ref $_ eq 'HASH' ? $_->{$field}
                                                                 : UNIVERSAL::can($_, $field) ? $_->$field()
                                                                 : $_)]} @$list ]
        : [map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, lc $_]} @$list ]; # case insensitive
}

sub vmethod_splice {
    my ($ref, $i, $len, @replace) = @_;
    @replace = @{ $replace[0] } if @replace == 1 && ref $replace[0] eq 'ARRAY';
    if (defined $len) {
        return [splice @$ref, $i || 0, $len, @replace];
    } else {
        return [splice @$ref, $i || 0];
    }
}

sub vmethod_split {
    my ($str, $pat, @args) = @_;
    $str = '' if ! defined $str;
    return defined $pat ? [split $pat, $str, @args] : [split ' ', $str, @args];
}

sub filter_eval {
    my $context = shift;
    return sub {
        my $text = shift;
        return $context->process(\$text);
    };
}

sub filter_redirect {
    my ($context, $file, $options) = @_;
    my $path = $context->config->{'OUTPUT_PATH'} || $context->throw('redirect', 'OUTPUT_PATH is not set');

    return sub {
        my $text = shift;
        if (! -d $path) {
            require File::Path;
            File::Path::mkpath($path) || $context->throw('redirect', "Couldn't mkpath \"$path\": $!");
        }
        local *FH;
        open (FH, ">$path/$file") || $context->throw('redirect', "Couldn't open \"$file\": $!");
        if (my $bm = (! $options) ? 0 : ref($options) ? $options->{'binmode'} : $options) {
            if (+$bm == 1) { binmode FH }
            else { binmode FH, $bm}
        }
        print FH $text;
        close FH;
        return '';
    };
}

###----------------------------------------------------------------###
### "here be dragons"

package CGI::Ex::_literal;
sub call { ${ $_[0] } }
sub set {}
sub does_autobox { 1 }

package CGI::Ex::_autobox;
sub call { $_[0]->[0]->call($_[1]) }
sub set {}
sub does_autobox { 1 }

package CGI::Ex::_concat;
sub call { join "", grep {defined} map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] } }
sub set {}
sub does_autobox { 1 }

package CGI::Ex::_hash;
sub call { return {map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] }} }
sub set {}
sub does_autobox { 1 }

package CGI::Ex::_array;
sub call { return [map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] }] }
sub set {}
sub does_autobox { 1 }

package CGI::Ex::_set;
sub call {
    my ($var, $val) = @{ $_[0] };
    $val = CGI::Ex::Var::get_exp($val, $_[1]);
    CGI::Ex::Var::set_exp($var, $val, $_[1]);
    return $val;
}
sub set {}
sub does_autobox { 1 }


package CGI::Ex::_not;
sub call { ! (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) || '' }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_and;
sub call { (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  &&  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_or;
sub call { ((ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ||  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1])) || '' }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_ifelse;
sub call {
    (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])
        ? (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1])
        : (ref($_[0]->[2]) ? $_[0]->[2]->call($_[1]) : $_[0]->[2]);
}
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_str_lt;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  lt  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_str_gt;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  gt  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_str_le;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  le  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_str_ge;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ge  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_eq;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  eq  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_ne;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ne  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_negate;
sub call { local $^W; 0 - (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_pow;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  **  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_mult;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  *   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_div;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  /   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_intdiv;
sub call { local $^W; int( (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  /   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) ) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_mod;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  %   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_plus;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  +   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_subtr;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  -   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_num_lt;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  <   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_num_gt;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  >   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_num_le;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  <=  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_num_ge;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  >=  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
sub set {}
sub does_autobox { 0 }

package CGI::Ex::_range;
sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) || 0 .. (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) || 0 }
sub set {}
sub does_autobox { 0 }

###----------------------------------------------------------------###

=head1 DESCRIPTION

Experimental.  An attempt for abstracting out a fast parser and hash
from CGI::Ex::Template.  It is functional - but currently too
cumbersome for use in CET.

=cut

1;
