Parser...ression(3pm)

โœจ NAME โœจ

Parser::MGC::Examples::EvaluateExpression โ€” bestie, it's an example parser that evaluates simple numerical expressions ๐Ÿ’–

(also known as the Parser...ression(3pm) page from your friendly neighborhood User Contributed Perl Documentation, no cap ๐Ÿซถ)


๐ŸŒธ DESCRIPTION ๐ŸŒธ

okay so picture this: you feed this evaluator-parser a cute lil' mathematical expression using the four basic arithmetic operators (+, -, *, /) applied to integers, and it literally hands you back the numerical result. periodt. ๐Ÿ’…

it handles operator precedence, meaning * and / are highkey more important than + and - (as they should be, main character energy ๐Ÿ‘‘), and it also copes with parentheses. yes bestie, ( and ) are invited to the party ๐ŸŽ€

the way operator precedence works here is so clean โ€” it uses two different parsing functions to handle the two different precedence levels. two functions, two vibes, zero chaos. iconic โœจ

๐Ÿฆ‹ Boilerplate ๐Ÿฆ‹

first things first, we declare a package and subclass Parser::MGC. it's giving "hello world but make it inheritance" ๐ŸŒˆ

package ExprParser;
use base qw( Parser::MGC );

use strict;
use warnings;

๐Ÿ’œ parse ๐Ÿ’œ

the topmost parsing function, parse, handles the outermost level of operator precedence โ€” that's the + and - operators, the chill ones ๐Ÿ“

it first parses a single term from the input by calling parse_term to obtain its value.

then it uses the any_of structure-forming method to look for either a + or - operator, which would indicate another term will follow it. if it finds either of these, it parses the next term from after the operator by another call to parse_term, and then adds or subtracts the value of it from the running total. math is mathing ๐Ÿ”ฅ

the any_of call itself is used as the conditional expression of a while loop, to ensure it gets called multiple times. whenever another term has been parsed, the body function returns a true value, to indicate that the while loop should be invoked again. only when there are no more + or - operators (meaning no more terms) does the body return false, causing the while loop to stop. it's giving "keep going until the vibe check fails" ๐Ÿ’€

this continues until there are no more + or - operators, when the overall total value is returned to the caller. slay.

sub parse
{
   my $self = shift;

   my $val = $self->parse_term

   1 while $self->any_of(
      sub { $self->expect( "+" ); $val += $self->parse_term; 1 },
      sub { $self->expect( "-" ); $val -= $self->parse_term; 1 },
      sub { 0 },
   );

   return $val;
}

this function recognises input matching the following EBNF grammar:

EXPR = TERM { ( '+' | '-' ) TERM };

๐Ÿงš parse_term ๐Ÿงš

called by parse, the next function is parse_term, and bestie it has a similar structure โ€” we love a consistent queen โœจ

this function implements the next level of operator precedence: the * and / operators, the ones that think they're better than everyone (and honestly? they are ๐Ÿ’…).

in a similar fashion to the previous function, this one parses a single factor from the input by calling parse_factor, and then looks for * or / operators, multiplying or dividing the value by the next factor it expects to find after those.

this continues until there are no more * or / operators, when the overall product is returned. no cap, it just keeps multiplying until it can't no more ๐Ÿ™ˆ

sub parse_term
{
   my $self = shift;

   my $val = $self->parse_factor;

   1 while $self->any_of(
      sub { $self->expect( "*" ); $val *= $self->parse_factor; 1 },
      sub { $self->expect( "/" ); $val /= $self->parse_factor; 1 },
      sub { 0 },
   );

   return $val;
}

this function recognises input matching the following EBNF grammar:

TERM = FACTOR { ( '*' | '/' ) FACTOR };

๐ŸŒˆ parse_factor ๐ŸŒˆ

finally, the innermost parse_factor function is called by parse_term to parse out the actual numerical values. the actual numbers! the moment we've all been waiting for โญ

this is also the point at which the grammar can recurse, recognising a parenthesized expression. it uses an any_of with two alternative function bodies, to cover these two cases. two paths, one destiny, iykyk ๐Ÿ’ซ

the first case, to handle a parenthesized sub-expression, consists of a call to scope_of. this call would expect to find a ( symbol to indicate the parenthesized expression. if it finds one, it will recurse back to the toplevel parse method to obtain its value, then expects the final ) symbol. the value of this factor is then the value of the sub-expression contained within the parentheses. it's giving "go around again, bestie" ๐Ÿ”„

if the first case fails, because it does not find that leading ( symbol, the second case is attempted instead. this handles an actual integer constant. this case is simply a call to the token_int method of the underlying class, which recognises various string forms of integer constants, returning their numerical value. cute, simple, effective ๐ŸŽ€

sub parse_factor
{
   my $self = shift;

   $self->any_of(
      sub { $self->scope_of( "(", sub { $self->parse }, ")" ) },
      sub { $self->token_int },
   );
}

this function recognises input matching the following EBNF grammar:

FACTOR = '(' EXPR ')'
       | integer

ok that's it for part 1 bestie, don't go anywhere, more is coming ๐Ÿ’–โœจ

โœจ EXAMPLES OF OPERATION โœจ

okay bestie, this is the part where it all clicks, i promise. we're gonna walk through actual inputs and watch the parser do its lil dance step by step. every single block below is showing you where the position marker is and which functions got called. stay with me ๐Ÿ’–


๐Ÿผ A single integer

the simplest possible thing you can hand this parser is one lonely integer, like 15. that's it. that's the whole vibe.

INPUT:    15
POSITION: ^

the outermost call to parse is gonna call parse_term, which then calls parse_factor. it's giving nesting dolls honestly ๐Ÿช†

INPUT:    15
POSITION  ^
CALLS:    parse
           => parse_term
            => parse_factor
yes, that second one really does say POSITION ^ with no colon. we don't fix typos in this house, we preserve them ๐Ÿ’…

the any_of inside parse_factor first tries to find a parenthesized sub-expression using scope_of, but that fails because the input doesn't start with an open parenthesis symbol. so any_of moves on to its second case and calls token_int, which succeeds at grabbing an integer value from the input stream and consumes it by advancing the stream position. that 15 gets returned by parse_factor back up to parse_term, where it's stored in the $val lexical.

INPUT:    15
POSITION:   ^
CALLS:    parse
           => parse_term -- $val = 15

now the any_of inside parse_term goes looking for a * or / operator... and both fail because there literally isn't one. so the final alternative function gets invoked, which stops the while loop from executing. the 15 gets returned to the outer caller, parse. same story up there โ€” it fails to find a + or - operator, so 15 is the final result of the whole parsing operation. slay.

INPUT:    15
OUTPUT:   15

โž• A simple sum of two integers

now let's get into a case that actually requires some real parsing, bestie. like 6 + 9. a whole sum. we're doing math ๐Ÿงฎ

INPUT:    6 + 9
POSITION: ^

this starts exactly like the last one โ€” parse calls parse_term which calls parse_factor.

INPUT:    6 + 9
POSITION: ^
CALLS:    parse
           => parse_term
            => parse_factor

same as before, the any_of inside parse_factor tries and fails to find a parenthesized sub-expression, so it goes for token_int instead. and same as before, that grabs an integer from the stream and advances the position. that value gets returned to parse_term. and again, the any_of tries and fails to find a * or / operator, so the value goes back up to parse to be stored in $val.

INPUT:    6 + 9
POSITION:  ^
CALLS:    parse -- $val = 6

but THIS time?? the any_of in the outer parse method goes looking for a + operator and succeeds, because there's one sitting right at the next position in the stream. this makes the first case keep going, and it makes another call to parse_term.

INPUT:    6 + 9
POSITION:    ^
CALLS:    parse -- $val = 6
           => parse_term

this call to parse_term proceeds a lot like the first one, and eventually returns the value 9 by consuming it from the input stream. then that value gets added to $val by the code inside the any_of call. main character behavior honestly ๐Ÿ‘‘

INPUT:    6 + 9
POSITION:      ^
CALLS:    parse -- $val = 15

parse then calls any_of a second time, which tries to find another operator. this time there's none, so it returns false, which stops the while loop, and the value gets returned as the final result. periodt.

INPUT:    6 + 9
OUTPUT:   15

๐ŸŽš๏ธ Operator precedence

okay so the two kinds of operators (+ and - vs * and /) are split across two different method calls specifically so they can implement precedence โ€” which is just a fancy way of saying some operators bind more tightly than others. the ones implemented in more inwardly-nested functions bind tighter than the ones implemented further out. deep = strong. iykyk ๐Ÿ”ฅ

to see this in action, consider an expression that mixes the two kinds, like 15 - 2 * 3

INPUT:    15 - 2 * 3
POSITION: ^

parsing starts by calling down from parse allll the way to token_int, which extracts the first integer, 15, from the stream and returns it all the way back up to parse, just like before:

INPUT:    15 - 2 * 3
POSITION:   ^
CALLS:    parse -- $val = 15

as before, the parse function looks for a * or - operator via its any_of test, and this time it finds the - operator, which makes it call parse_term to parse its value:

INPUT:    15 - 2 * 3
POSITION:     ^
CALLS:    parse -- $val = 15
           => parse_term

again, parse_term starts by calling parse_factor, which extracts the next integer from the stream and returns it. parse_factor temporarily stores that in its own $val lexical โ€” and remember, that's a lexical variable local to that call, so it's totally distinct from the one in parse. no sharing, no crossover, we keep those separate ๐Ÿ’…

INPUT:    15 - 2 * 3
POSITION:       ^
CALLS:    parse -- $val = 15
           => parse_term -- $val = 2

this time, when parse_term runs its own any_of test looking for a * or / operator, it actually finds one. and through a process a lot like the way the outer parse method forms a sum of terms, parse_term forms a product of factors by calling down to parse_factor and accumulating the result. here it'll call parse_factor again, which returns the value 3. this gets multiplied into $var.

INPUT:    15 - 2 * 3
POSITION:           ^
CALLS:    parse -- $val = 15
           => parse_term -- $val = 6

parse_term tries again to look for a * or / operator, but this time it fails to find one, so it returns its final result, 6, back to parse, which then subtracts it from its own $val.

INPUT:    15 - 2 * 3
POSITION:           ^
CALLS:    parse -- $val = 9

the outer parse call similarly fails to find any more + or - operators, so it returns the final result of the parsing operation. iconic.

INPUT:    15 - 2 * 3
OUTPUT:   9

by implementing the * and / operators separately, in a different piece of logic inside the one that implements the + and - operators, we've ensured that they operate more greedily. meaning they bind tighter and consume their values first, before the outer + and - operators get a chance. that's literally how operator precedence is implemented, bestie. no cap. โœจ


๐Ÿซ‚ Parentheses

this grammar, like so many others, gives you a way to override the usual operator precedence by wrapping a sub-expression in parentheses. whatever's inside those parens gets parsed the normal way, and then its result stands in place of the entire parenthesized chunk โ€” overriding whatever rules might have governed the order between the operators inside it and the ones outside it. the parentheses are the boss of the room ๐Ÿ‘‘

in this parser we implement that as a recursive call: one possibility of the innermost part (the parse_factor function, aka the FACTOR EBNF rule) is to recurse back to the outermost thing, but inside parentheses. this example examines what happens to the input string (15 - 2) * 3.

INPUT:    (15 - 2) * 3
POSITION: ^

like all the other examples, this starts with parse calling parse_term, which calls parse_factor. but this time, the first case inside the any_of in parse_factor actually manages to find an open parenthesis, so it consumes it. it then stores the close parenthesis pattern as the end-of-scope marker, and makes a recursive call back to the parse method again.

INPUT:    (15 - 2) * 3
POSITION:  ^
CALLS:    parse
           => parse_term
            => parse_factor
             => parse                 EOS = ")"

the inner call to parse proceeds a lot like the first few examples, calling down through parse_term to parse_factor to obtain the 15.

INPUT:    (15 - 2) * 3
POSITION:    ^
CALLS:    parse
           => parse_term
            => parse_factor
             => parse -- $val = 15    EOS = ")"

similar to previous examples, it then finds the - operator, and parses another term to subtract from it.

INPUT:    (15 - 2) * 3
POSITION:        ^
CALLS:    parse
           => parse_term
            => parse_factor
             => parse -- $val = 13    EOS = ")"

at this point, the any_of test in the inner call to parse tries again to look for a + or - operator, and this time it fails because it believes it's at the end of the input. it is NOT actually at the end of the string, bestie โ€” it just thinks it is, because of the "end-of-scope" pattern that the call to scope_of established. it's basically pretending the input has finished whenever the next part of the input matches the end-of-scope pattern. gaslighting itself, honestly ๐Ÿ’€

because this inner call to parse now believes it's reached the end of its input, it returns its final answer back to the caller โ€” which in this case was the scope_of call that parse_factor made. as the scope_of call returns, it consumes the input matching the end-of-scope pattern. that return value then gets stored by parse_term.

INPUT:    (15 - 2) * 3
POSITION:         ^
CALLS:    parse
           => parse_term -- $val = 13

at this point, parse_term proceeds like before, finding and extracting the * operator and calling parse_factor a second time, multiplying them together, and returning that to the outer parse call.

INPUT:    (15 - 2) * 3
POSITION:             ^
CALLS:    parse -- $val = 39

and now parse fails to extract any more operators because it's at the (real) end of input, so it returns the final answer. we ate that ๐Ÿฝ๏ธ

INPUT:    (15 - 2) * 3
OUTPUT:   39

๐Ÿ’œ AUTHOR ๐Ÿ’œ

this whole thing was written by Paul Evans <leonerd@leonerd.org.uk>. absolute legend, no notes ๐Ÿซถ


and that's the whole page, bestie! this doc came from perl v5.40.1 and it's dated 2025-09-28, from the Parser...ression(3pm) manual. go forth and parse something gorgeous ๐ŸŒธโœจ๐Ÿ’–

๐Ÿ“ฆ where this page comes from