// 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}") }