calc/src/main.nika

// 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

This site is open source. Improve this page.