Expression Evaluator
This guide builds a math expression evaluator supporting arithmetic (+, -, *, /), exponentiation (**), unary minus, parentheses, constants (pi), and functions (sin, atan2). It demonstrates Alpaca's operator precedence and conflict resolution.
What you'll learn: dynamic token names, named productions, the before/after DSL, and how to express a complete precedence hierarchy.
The Lexer
import alpaca.*
val CalcLexer = lexer:
case "\\s+" => Token.Ignored
case "#.*" => Token.Ignored
case "\\*\\*" => Token["exp"] // multi-char operators before single-char
case literal @ ("\\+" | "-" | "\\*" | "/" | "\\(" | "\\)" | ",") =>
Token[literal.type]
case keyword @ ("pi" | "sin" | "atan2") =>
Token[keyword.type]
case x @ """(\d+\.\d*|\.\d+)([eE][+-]?\d+)?""" => Token["float"](x.toDouble)
case x @ "\\d+" => Token["int"](x.toInt)
Note that "\\*\\*" (exponentiation) must appear before "\\*" (multiplication) to avoid shadowing.
The Parser
Named productions ("plus", "times", etc.) let us reference specific alternatives in conflict resolution:
import alpaca.*
object CalcParser extends Parser:
val root: Rule[Double] = rule:
case Expr(v) => v
val Expr: Rule[Double] = rule(
"plus" { case (Expr(a), CalcLexer.`\\+`(_), Expr(b)) => a + b },
"minus" { case (Expr(a), CalcLexer.`-`(_), Expr(b)) => a - b },
"times" { case (Expr(a), CalcLexer.`\\*`(_), Expr(b)) => a * b },
"divide" { case (Expr(a), CalcLexer.`/`(_), Expr(b)) => a / b },
"exp" { case (Expr(a), CalcLexer.`exp`(_), Expr(b)) => math.pow(a, b) },
"uminus" { case (CalcLexer.`-`(_), Expr(a)) => -a },
"pi" { case CalcLexer.pi(_) => math.Pi },
"sin" { case (CalcLexer.sin(_), CalcLexer.`\\(`(_), Expr(a), CalcLexer.`\\)`(_)) => math.sin(a) },
"atan2" {
case (CalcLexer.atan2(_), CalcLexer.`\\(`(_), Expr(y), CalcLexer.`,`(_), Expr(x), CalcLexer.`\\)`(_)) =>
math.atan2(y, x)
},
{ case (CalcLexer.`\\(`(_), Expr(a), CalcLexer.`\\)`(_)) => a },
{ case CalcLexer.float(x) => x.value },
{ case CalcLexer.int(n) => n.value.toDouble },
)
Without conflict resolution, this grammar is ambiguous -- the compiler reports shift/reduce conflicts for every binary operator.
Conflict Resolution
override val resolutions = Set(
// Exponentiation: right-associative, highest binary precedence
CalcLexer.exp.before(production.uminus, production.exp, production.times, production.divide),
production.exp.before(CalcLexer.`\\*`, CalcLexer.`/`),
// Unary minus binds tighter than * /
production.uminus.before(CalcLexer.`\\*`, CalcLexer.`/`),
// Multiplication/division: left-associative, higher than +/-
production.times.before(CalcLexer.`\\*`, CalcLexer.`/`),
production.divide.before(CalcLexer.`\\*`, CalcLexer.`/`),
// Addition/subtraction: left-associative, lowest binary precedence
production.plus.after(CalcLexer.`\\*`, CalcLexer.`/`),
production.plus.before(CalcLexer.`\\+`, CalcLexer.`-`),
production.minus.after(CalcLexer.`\\*`, CalcLexer.`/`),
production.minus.before(CalcLexer.`\\+`, CalcLexer.`-`),
)
The precedence hierarchy from highest to lowest: ** > unary - > * / > + -.
before(tokens)= prefer reducing this production over shifting those tokensafter(tokens)= prefer shifting those tokens over reducing this productionToken.before(productions)= prefer shifting this token over reducing those productions
Running It
val input = "sin(pi / 2) + 2 ** 3 * 4"
val (_, lexemes) = CalcLexer.tokenize(input)
val (_, result) = CalcParser.parse(lexemes)
println(result) // 33.0 (1.0 + 8.0 * 4.0)
Exercises
- Add a modulo operator (
%) with the same precedence as*and/ - Add variable assignment (
x = expr) with right-associative binding - Add
cos,tan, andsqrtfunctions
