Build a Calculator Grammar
Parsing arithmetic expressions with correct operator precedence is a classic parsing challenge. Raku grammars handle it elegantly. In this post, we will build a calculator that correctly evaluates expressions like3 + 4 2 - (10 / 5).
The Precedence Problem
The naive approach of parsing left to right fails because3 + 4 2 should equal 11 (not 14). Multiplication binds tighter than addition. We handle this by structuring our grammar into precedence levels.
The Grammar
Each precedence level gets its own rule. Lower-precedence operators sit higher in the grammar:The key insight:grammar Calculator { rule TOP { <expression> } # Lowest precedence: addition and subtraction rule expression { <term>+ % <add-op> } token add-op { '+' | '-' } # Higher precedence: multiplication and division rule term { <factor>+ % <mul-op> } token mul-op { '*' | '/' } # Highest precedence: atoms and parenthesized expressions rule factor { <number> | '(' <expression> ')' } token number { '-'? \d+ ['.' \d+]? } }
expression is built from terms separated by + or -. Each term is built from factors separated by * or /. And a factor is either a number or a parenthesized expression (which recurses back to the top).
Testing the Grammar
Let us verify our grammar parses correctly:All of these should produce match objects. But to actually compute results, we need actions.say Calculator.parse("3 + 4 * 2"); say Calculator.parse("(3 + 4) * 2"); say Calculator.parse("10 / 2 - 3"); say Calculator.parse("1 + 2 + 3 + 4");
The Action Class
class Calculator-Actions { method TOP($/) { make $<expression>.made; } method expression($/) { my @terms = $<term>.map(*.made); my @ops = $<add-op>.map(*.); my $result = @terms.shift; for @ops Z @terms -> ($op, $val) { given $op { when '+' { $result += $val } when '-' { $result -= $val } } } make $result; } method term($/) { my @factors = $<factor>.map(*.made); my @ops = $<mul-op>.map(*.); my $result = @factors.shift; for @ops Z @factors -> ($op, $val) { given $op { when '*' { $result *= $val } when '/' { $result /= $val } } } make $result; } method factor($/) { if $<number> { make $<number>.made; } else { make $<expression>.made; } } method number($/) { make $/.; } }
Putting It Together
sub calc( $expr --> Numeric) { my $result = Calculator.parse($expr, actions => Calculator-Actions.new); die "Parse error: '$expr'" unless $result; $result.made; } say calc("3 + 4 * 2"); # 11 say calc("(3 + 4) * 2"); # 14 say calc("10 / 2 - 3"); # 2 say calc("1 + 2 + 3 + 4"); # 10 say calc("2 * 3 + 4 * 5"); # 26 say calc("(2 + 3) * (4 + 5)");# 45 say calc("100 / 10 / 2"); # 5
Adding Exponentiation
Let us add a power operator^ with higher precedence than multiplication:
grammar CalcPlus { rule TOP { <expression> } rule expression { <term>+ % <add-op> } token add-op { '+' | '-' } rule term { <power>+ % <mul-op> } token mul-op { '*' | '/' } # New level for exponentiation rule power { <factor> ['^' <factor>]* } rule factor { <number> | '(' <expression> ')' } token number { '-'? \d+ ['.' \d+]? } } class CalcPlus-Actions { method TOP($/) { make $<expression>.made } method expression($/) { my @terms = $<term>.map(*.made); my @ops = $<add-op>.map(*.); my $result = @terms.shift; for @ops Z @terms -> ($op, $val) { $result = $op eq '+' ?? $result + $val !! $result - $val; } make $result; } method term($/) { my @powers = $<power>.map(*.made); my @ops = $<mul-op>.map(*.); my $result = @powers.shift; for @ops Z @powers -> ($op, $val) { $result = $op eq '*' ?? $result * $val !! $result / $val; } make $result; } method power($/) { my @factors = $<factor>.map(*.made); # Right-associative: 2^3^2 = 2^(3^2) = 512 make [R**] @factors; } method factor($/) { make $<number> ?? $<number>.made !! $<expression>.made; } method number($/) { make $/. } } sub calc-plus( $expr) { my $r = CalcPlus.parse($expr, actions => CalcPlus-Actions.new); die "Parse error" unless $r; $r.made; } say calc-plus("2 ^ 10"); # 1024 say calc-plus("2 ^ 3 ^ 2"); # 512 (right-associative) say calc-plus("3 + 2 ^ 3 * 2"); # 19 (= 3 + 8*2 = 3 + 16)
Adding Unary Minus
Handle negative numbers and negation:# In the grammar, update factor: # rule factor { '-'? [ <number> | '(' <expression> ')' ] } # In actions: # method factor($/) { # my $val = $<number> ?? $<number>.made !! $<expression>.made; # make $/.Str.starts-with('-') ?? -$val !! $val; # }
Error Reporting
Add meaningful error messages when parsing fails:sub calc-safe( $expr) { my $r = Calculator.parse($expr, actions => Calculator-Actions.new); unless $r { # Find where parsing stopped my $partial = Calculator.subparse($expr); my $pos = $partial ?? $partial.to !! 0; die "Parse error at position $pos in: $expr\n" ~ " " x ($pos + 1) ~ "^-- here"; } $r.made; } try { say calc-safe("3 + * 2"); CATCH { default { say .message } } }
The Pattern
The grammar structure for precedence climbing follows a consistent pattern:- Each precedence level is a rule that combines the next-higher level with its operator
- The highest level handles atoms (numbers, identifiers) and parenthesized expressions
- Parenthesized expressions recurse back to the lowest precedence level
This pattern extends to any number of precedence levels. To add a new operator, just insert a new level in the right spot.
This technique is the foundation for building real language parsers in Raku. Next time we will apply it to parsing a real-world format: CSV.