// A four-function calculator, written as a grammar.
//
// nikaia run -- "2 + 3 * (10 - 4) / 2"
// 2 + 3 * (10 - 4) / 2 = 11
//
// Shows: a `grammar` block, precedence and recursion as rules, an `enum` and a
// tuple pattern in `match`, and `catch` on a parse that fails.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints against `tests/` (ADR-269 D13) and runs the `test` blocks
// at the end of this file.
use std::cli
// Two operators, and the type says two: a `match` over it needs no third arm.
enum Op {
Times,
Divide,
}
grammar Calc {
// A lowercase rule is *syntactic*: whitespace between its parts is skipped,
// so "2+3" and "2 + 3" both parse and no rule mentions blanks.
//
// `a - b - c` means `(a - b) - c`: the tails are collected and folded left to
// right in the action. (`expr = expr "-" term` would never terminate.)
entry rule expr -> i64 =
head:term tail:add_tail* -> {
let mut value = head
for t in tail { value = value + t }
value
}
// Subtraction is addition of a negation, so the fold above needs no operator.
rule add_tail -> i64 =
"+" t:term -> { t }
| "-" t:term -> { -t }
// Division cannot be folded that way, so each operand travels with its
// operator as a pair `(Op, i64)`, and `match` takes the pair apart.
// `term` sits between `expr` and `factor`, which is why `*` binds tighter
// than `+`: precedence is the order of the rules, not a table.
rule term -> i64 =
head:factor tail:mul_tail* -> {
let mut value = head
for step in tail {
match step {
(Op::Times, n) => { value = value * n }
(Op::Divide, n) => { value = value / n }
}
}
value
}
rule mul_tail -> (Op, i64) =
op:mul_op f:factor -> { (op, f) }
rule mul_op -> Op =
"*" -> { Op::Times }
| "/" -> { Op::Divide }
// The recursion: `expr` is reached again inside the parentheses, so
// `(10 - 4)` is a factor and multiplies like one.
//
// `# "expression"` names the rule in error messages: `2 +` reports
// `expected expression` rather than listing every way an operand can start.
rule factor -> i64 # "expression" =
"(" e:expr ")" -> { e }
| "-" f:factor -> { -f }
| n:NUMBER -> { n }
// An uppercase rule is *lexical*: nothing is skipped between the digits, so
// "1 2" is not one number. `dec[i64](…)` turns the digits into an `i64`; a
// number too large for one is a parse error, not a silent wrap.
rule NUMBER -> i64 =
n:dec[i64](digit+) -> { n }
}
fn main() throws {
// The expression from the command line, or an example.
let source = cli::args().nth(1) ?? "2 + 3 * (10 - 4) / 2"
// The grammar is the parser: `Calc::expr` parses the whole string. Input it
// rejects is handled by `catch` like any other failure.
let value = Calc::expr(source) catch {
eprintln(f"{source}: {error}")
return
}
println(f"{source} = {value}")
}
// **What the grammar promises, as tests** (ADR-269 D1): `nikaia test` runs
// each in a process of its own, and `nikaia build` leaves them out.
test "multiplication binds tighter than addition" {
let value = Calc::expr("2 + 3 * 4")
assert(value == 14)
}
test "subtraction and division fold from the left" {
let difference = Calc::expr("10 - 4 - 3")
let quotient = Calc::expr("100 / 10 / 5")
assert(difference == 3)
assert(quotient == 2)
}
test "a missing operand is refused" {
let value = Calc::expr("2 +") catch { return }
panic(f"`2 +` parsed, as {value}")
}
The file itself · all the examples