Raku Books / Perl 6 at a Glance / Regexes and Grammars

Calculator

When considering language parsers, implementing a calculator is like writing a “Hello, World!” programme. In this section, we will create a grammar for the calculator that can handle the four arithmetical operations and parentheses. The hidden advantage of the calculator example is that you have to teach it to follow the operations priority and nested expressions.

Our calculator grammar will expect the single expression at a top level. The priority of operations will be automatically achieved by the traditional approach to grammar construction, in which the expression consists of terms and factors.

The terms are parts separated by pluses and minuses:

<term>+ %% ['+'|'-']

Here, Perl 6’s %% symbol is used. You may also rewrite the rule using more traditional quantifiers:

<term> [['+'|'-'] <term>]*

Each term is, in turn, a list of factors separated by the symbols for multiplication or division:

<factor>+  %% ['*'|'/']

CodeBlockPlaceholder4raku-static rule group { ‘(’ ‘)’ } CodeBlockPlaceholder5

This rule refers to the expression rule and thus can start another recursion loop.

It’s time to introduce the enhancement of the value token so that it accepts the floating point values. This task is easy; it only requires creating a regex that matches the number in as many formats as possible. I will skip the negative numbers and the numbers in scientific notation.

token value {
    | \d+['.' \d+]?
    | '.' \d+
}

Here is the complete grammar of the calculator:

grammar Calc {
    rule TOP {
        ^ <expression> $
    }
    rule expression {
        | <term>+ %% $<op>=(['+'|'-'])
        | <group>
    }
    rule term {
        <factor>+  %% $<op>=(['*'|'/'])
    }
    rule factor {
        | <value>
        | <group>
    }
    rule group {
        '(' <expression> ')'
    }
    token value {
        | \d+['.' \d+]?
        | '.' \d+
    }
}

Note the $<op>=(...) construction in some of the rules. This is the named capture. The name simplifies the access to a value via the $/ variable. In this case, you can reach the value as $<op>, and you don’t have to worry about the possible change of the variable name after you update a rule as it happens with the numbered variables $0, $1, etc.

Now, create the actions for the compiler. At the TOP level, the rule returns the calculated value, which it takes from the ast field of the expression.

class CalcActions {
    method TOP($/) {
        $/.make: $<expression>.ast
    }
    . . .
}

The actions of the underlying rules groups and value are as simple as we’ve just seen.

method group($/) {
    $/.make: $<expression>.ast
}

method value($/) {
    $/.make: +$/
}

CodeBlockPlaceholder10raku-static method factor($/) { if $ { $/.make: +$ } else { $/.make: $.ast } } CodeBlockPlaceholder11

Move on to the term action. Here, we have to take care of the list with its variable length. The rule’s regex has the + quantifier, which means that it can capture one or more elements. Also, as the rule handles both the multiplication and the division operators, the action must distinguish between the two cases. The $<op> variable contains either the * or the / character.

This is how the syntax tree looks like for the string with three terms, 3*4*5:

expression => 「3*4*5」
 term => 「3*4*5」
  factor => 「3」
   value => 「3」
  op => 「*」
  factor => 「4」
   value => 「4」
  op => 「*」
 factor => 「5」
  value => 「5」

CodeBlockPlaceholder13raku-static method term($/) { my $result = $[0].ast;

if $<op> {
    my @ops = $<op>.map(~*);
    my @vals = $<factor>[1..*].map(*.ast);

    for 0..@ops.elems - 1 -> $c {
        if @ops[$c] eq '*' {
            $result *= @vals[$c];
        }
        else {
            $result /= @vals[$c];
        }
    }
}

$/.make: $result;

} CodeBlockPlaceholder14raku-static my @ops = $.map(~*); CodeBlockPlaceholder15

The values themselves will land in the @vals array. To ensure that the values of the two arrays, @vals and @ops, correspond to each other, the slice of $<factor> is taken, which starts at the second element:

my @vals = $<factor>[1..*].map(*.ast);

CodeBlockPlaceholder17raku-static method expression($/) { if $ { $/.make: $.ast } else { my $result = $[0].ast;

    if $<op> {
        my @ops = $<op>.map(~*);
        my @vals = $<term>[1..*].map(*.ast);
        for 0..@ops.elems - 1 -> $c {
            if @ops[$c] eq '+' {
                $result += @vals[$c];
            }
            else {
                $result -= @vals[$c];
            }
        }
    }
     $/.make: $result;
}

} CodeBlockPlaceholder18raku-static my $calc = Calc.parse( @*ARGS [0], :actions(CalcActions) ); say $calc.ast; CodeBlockPlaceholder19

Let’s see if it works.

$ perl6 calc.pl '39 + 3.14 * (7 - 18 / (505 - 502)) - .14'
42

It does.

CodeBlockPlaceholder21raku-static

x = 40 + 2; print x;

y = x - (5/2); print y;

z = 1 + y * x; print z;

print 14 - 16/3 + x; ```

Course navigation

AST and attributes   |   Appendix