samizdat has asked for the wisdom of the Perl Monks concerning the following question:

After getting pretty far along with To model or not to model, I've decided that making the application general enough to meet the requirements requires a change to a parsed-grammar approach. I've rewritten almost everything as a Parse::RecDescent grammar.

Almost everything works correctly.

In one case, however, I get strange results back from the @item array, like this:
19|ifthenelse|>>Matched action<< (return value: []) | 19|ifthenelse|>>Matched production: [if elsif else | | |endif]<< | 19|ifthenelse|>>Matched rule<< (return value: | | |[ARRAY(0x870cb08)]) | 19|ifthenelse|(consumed: []) |
from source text:
if stw!=0 then stw else 1.40 * 1e-6 endif
and the following (simplified) grammar:
expr: ifthenelse { $return = main::evaluate($main::c_v, @item); } ifthenelse: if elsif(s?) else(?) endif { $return = 'NIL'; for (my $i = 1; $i < @item; $i++) { print " iftest: $i '" . $item[$i] . "'\n"; if ($item[$i] ne 'NIL') { $return = $item[$i]; last; } } if ($return eq 'NIL') { die "IF statement '@item' not resolvable! $!\n"; } } if: /if/i expr /then/i expr { print 'i2: ' . $item[2] ."\n"; if($item[2]) { $return = $item[4]; } else { $return = 'NIL'; + } } else: /else/i expr { $return = $item[2]; } elsif: /elsif/i expr /then/i expr { if ($item[2]) { $return = $item[4] } else { $return = 'NIL' +} } endif: /endif/i { $return = 'NIL'; }
and main 'engine' subroutine:
sub evaluate { my $contvar = shift; shift; my $estr = join(' ', @_); my @whatsleft = @_; print " evaluating '$estr'\n"; $estr =~ s/ARRAY\x28[^\x28\x29]+\x29//g; $estr = trim($estr); if ($estr !~ /^[\s\.\/\x28\x290-9eE*+-]+$/) { my $parser = SpiceGrammar->new() or die "Bad SpiceGrammar: $!\n"; print " evaluating variable\n"; defined(my $value = $parser->vardef($estr)) or die "Bad input: {$estr} $!\n"; if ($value eq '') { $value = 0; print " substituting\n"; } return $value; } else { my $value = eval($estr); $vartable{$contvar} = $value; return $value; } }
WHen the ELSE clause matched, it (apparently) saved the result properly:
21| expr |>>Matched action<< (return value: | | |[1.4e-06]) | 21| expr |>>Matched production: [number operator| | |expr]<< | 21| expr |>>Matched rule<< (return value: [1.4e-| | |06]) | 21| expr |(consumed: [ 1.40 * 1e-6]) | 20| else |>>Matched subrule: [expr]<< (return | | |value: [1.4e-06] | 20| else |Trying action | 20| else |>>Matched action<< (return value: | | |[1.4e-06]) | 20| else |>>Matched production: [/else/i expr]<<| 20| else |>>Matched rule<< (return value: [1.4e-| | |06]) | 20| else |(consumed: [ else 1.40 * 1e-6]) | 19|ifthenelse|>>Matched repeated subrule: [else]<< | | |(1 times)
but I can't see where it's going south and returning an array (or the string expression of an array). In other similar cases (outside the ifthenelse-decoder) sub-expression results are returned properly.

TIA for pointers or assistance! :D

Replies are listed 'Best First'.
Re: To make the Model WORK
by blokhead (Monsignor) on Jul 13, 2005 at 16:12 UTC
    but I can't see where it's going south and returning an array (or the string expression of an array). In other similar cases (outside the ifthenelse-decoder) sub-expression results are returned properly.
    ifthenelse: if elsif(s?) else(?) endif
    Since there may be more than one elsif, and else may be optional, these corresponding subexpressions return arrays (see Subrules from P::RD's docs). For example, in this production, $item[2] is an array of the semantic values of all the elsif nonterminals that were found at this point.

    Two points:

    • Use Data::Dumper instead of print statements to see what things are.
    • I find the way you've written your grammar slightly unintuitive (although I admit I'm not really sure what you're trying to accomplish in your semantic actions). I would write it more like this:
    use Parse::RecDescent; use Data::Dumper; my $parser = Parse::RecDescent->new(<<'END_GRAMMAR'); expr: /\d+/ { $item[1] } | ifthenelse { $item[1] } ifthenelse: "if" expr "then" expr elsif(s?) else(?) "endif" { [ if => [@item[2,4]], @{$item[5]}, ref $item[6] ? $item[6][0] : undef ] } elsif: "elsif" expr "then" expr { [ @item[2,4] ] } else: "else" expr { $item[2] } END_GRAMMAR for (<DATA>) { print $_; print Dumper $parser->expr($_); } __DATA__ if 1 then 2 else 3 endif if 1 then 2 endif if 1 then 2 elsif 3 then 4 elsif 5 then 6 endif if 1 then 2 elsif 3 then 4 else 5 endif
    This returns an array that is structured like this:
    [ "if", [ cond1, result1 ], # first "if" condition/result expression + pair [ cond2, result2 ], # 0 or more "elsif" expression pairs ... otherwise ] # else branch expression, or undef if no +ne
    This data structure is fairly uniform, and should be easy to evaluate for whatever it is you're doing.

    blokhead

      The function evaluating the parse tree can be simplified if you eliminate elsif at compile time. Keep in mind that

      if cond1 then action1 elsif cond2 then action2 elsif cond3 then action3 else action4 endif

      is equivalent to

      if cond1 then action1 else if cond2 then action2 else if cond3 then action3 else action4 endif endif endif

      except the latter is easier to evaluate because the parse tree is simply

      [ if => cond1, action1, [ if => cond2, action2, [ if => cond3, action3, action4 ] ] ]

      See my reply below (under the heading "The following returns a nice tree for expr to evaluate") for an implementation.

      Ahhh... *thanks*, blokhead. Thanks, also, for your comments. Of necessity, I've compressed my example way down (expr has 16 alternate forms, for example), but gems of wisdom are always worth my time to consider.
Re: To make the Model WORK
by ikegami (Patriarch) on Jul 13, 2005 at 16:20 UTC
    ifthenelse: if elsif(s?) else(?) endif { ... }

    Because of the (s), elsif(s?) returns a reference to an array of "stuff that elsif matches". The array may contain 0 or more elements. That means the line identified below will always return true for $item[2]

    ifthenelse: if elsif(s?) else(?) endif { $return = 'NIL'; for (my $i = 1; $i < @item; $i++) { print " iftest: $i '" . $item[$i] . "'\n"; if ($item[$i] ne 'NIL') { # Always true for $item[2]. $return = $item[$i]; # Always an array ref for $item[2]. last; # Always gets here for $item[2]. } } ... }

    Comments:

    By the way, why are you using 'NIL'? It would be more convenient to use a false value. Only undef will fail the production, so you can use 0 and '' safely.

    Also, do you realize the above allows for if if var then var endif then var endif? if is rarely considered an expression. The ternary conditional operator in Perl (... ? ... : ...) and in Excel (@IF(..., ..., ...)) are examples of where ifs are expressions.

    Finally, I find it very odd that you evaluate at compile time. For example, in
    if cond then action1 else action2 endif
    you unconditionally evaluate action1 first, then you unconditionally evaluate action2, then you decide which value to return based on cond. That is dangerous.

    The following returns a nice tree for expr to evaluate:

    expr : if { # I doubt this should be in the grammar. $return = evaluate($main::c_v, $item[1]); } if : /if/i expr /then/i expr if_(?) { [ if => @item[2, 4, 5] ] } if_ : elsif { $item[1] } | else { $item[1] } elsif : /elsif/i expr /then/i expr if_ { [ if => @item[2, 4, 5] ] } else : /else/i expr { $item[2] }
      25-year programmer showing graying roots. :) Real returned values might be '0', as well. That would fail.

      I'm fortunate that, the way I've built this, now all I need to do is to 'flatten' the @item from IFTHENELSE into a one-dimensional array and then iterate. The first non-NIL result found will be returned, and I don't care which elsif or else clause gave it to me. Thanks to you and blokhead both, I see light illuminating repeated subrules. :D
        Real returned values might be '0', as well. That would fail.

        Nope. '0' will not fail a production. Only undef.

        I stand corrected:

        use Parse::RecDescent; my $parser; $parser = Parse::RecDescent->new(q` test: { $return = 1 } `); print($parser->test('') ? 'ok' : 'fail', "\n"); # ok $parser = Parse::RecDescent->new(q` test: { $return = 0 } `); print($parser->test('') ? 'ok' : 'fail', "\n"); # fail $parser = Parse::RecDescent->new(q` test: { $return = '0' } `); print($parser->test('') ? 'ok' : 'fail', "\n"); # fail $parser = Parse::RecDescent->new(q` test: { $return = undef } `); print($parser->test('') ? 'ok' : 'fail', "\n"); # fail $parser = Parse::RecDescent->new(q` test: /\d+/ `); print($parser->test('1') ? 'ok' : 'fail', "\n"); # ok print($parser->test('0') ? 'ok' : 'fail', "\n"); # fail

        I never got into that situation, because I'd be returning
        [ ident   => ident ]
        or
        [ literal => literal ]
        or
        [ '*'     => expr, expr ]
        or
        [ '/'     => expr, expr ]
        or
        ...

      Finally, I find it very odd that you evaluate at compile time. For example, in if cond then action1 else action2 endif you unconditionally evaluate action1 first, then you unconditionally evaluate action2, then you decide which value to return based on cond. That is dangerous.



      My task is to reduce a whole bunch of SPICE equations from static files to parameter values. None of these have side-effects, so the 'danger' seems minimal. I built a monster hash with local and non-local variables and their values, and my goal is to recursively reduce them until I hit a variable which is undefined by the fab process team or resolves to divide-by-zero. The grammar is very ill-defined, but P::RD is actually handling it quite well.

      I wish I'd had an easier task than this for my first P::RD work, but, well, "Needs must when the customer drives." Thanks for all your patience and examples! I will peruse to grok further!:D