raku.gg / grammars

Build a Calculator Grammar

2026-03-30

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 like 3 + 4 2 - (10 / 5).

The Precedence Problem

The naive approach of parsing left to right fails because 3 + 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:
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+]? } }
The key insight: 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:
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");
All of these should produce match objects. But to actually compute results, we need actions.

The Action Class

class Calculator-Actions { method TOP($/) { make $<expression>;.made; } method expression($/) { my @terms = $<term>;.map(*.made); my @ops = $<add-op>;.map(*.Str); 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(*.Str); 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 $/.Num; } }

Putting It Together

sub calc(Str $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(*.Str); 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(*.Str); 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 $/.Num } } sub calc-plus(Str $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(Str $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:
  1. Each precedence level is a rule that combines the next-higher level with its operator
  2. The highest level handles atoms (numbers, identifiers) and parenthesized expressions
  3. 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.