Advanced Projects
Expression Parser
A Pratt parser (top-down operator precedence parser) parses expressions with arbitrary precedence levels, prefix unary operators (-x), and both left-associative (+, -, *, /) and right-associative (^ exponentiation) binary operators using a single parameterized recursion parse_bp(min_bp: u8) driven by binding power pairs (left_bp, right_bp).
Binding Power and Associativity Mechanics
Left-Associative Operator (`+`, bp = (3, 4)):
Input: a + b + c
1. At `a`, see `+` with l_bp = 3 >= min_bp(0). Recurse with min_bp = r_bp = 4.
2. Next `+` has l_bp = 3 < min_bp(4) -> loop breaks!
Result: ((a + b) + c)
Right-Associative Operator (`^`, bp = (8, 7)):
Input: 2 ^ 3 ^ 2
1. At `2`, see `^` with l_bp = 8 >= min_bp(0). Recurse with min_bp = r_bp = 7.
2. Next `^` has l_bp = 8 >= min_bp(7) -> inner recursion consumes `3 ^ 2` first!
Result: (2 ^ (3 ^ 2)) = 512
| Operator | Category | Binding Power (l_bp, r_bp) |
Associativity |
|---|---|---|---|
+, - |
Infix additive | (3, 4) |
Left (l_bp < r_bp) |
*, /, % |
Infix multiplicative | (5, 6) |
Left (l_bp < r_bp) |
^ |
Infix exponentiation | (8, 7) |
Right (l_bp > r_bp) |
Unary -, + |
Prefix unary | ((), 9) |
Prefix (binds tighter than ^ left operand? Or 7 if -2^2 = -(2^2)) |
f(arg1, arg2) |
Function call | Prefix atom + (...) |
Highest |
Complete Pratt Expression Parser and Evaluator
#[derive(Debug, Clone, PartialEq)]
pub enum ExprToken {
Number(f64),
Ident(String),
Op(char),
LParen,
RParen,
Comma,
Eof,
}
pub fn lex_expr(input: &str) -> Result<Vec<ExprToken>, String> {
let mut chars = input.chars().peekable();
let mut out = Vec::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
} else if c.is_ascii_digit() || c == '.' {
let mut s = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() || d == '.' {
s.push(d);
chars.next();
} else {
break;
}
}
let val: f64 = s.parse().map_err(|_| format!("invalid float '{s}'"))?;
out.push(ExprToken::Number(val));
} else if c.is_ascii_alphabetic() || c == '_' {
let mut ident = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_alphanumeric() || ch == '_' {
ident.push(ch);
chars.next();
} else {
break;
}
}
out.push(ExprToken::Ident(ident));
} else {
chars.next();
match c {
'+' | '-' | '*' | '/' | '^' => out.push(ExprToken::Op(c)),
'(' => out.push(ExprToken::LParen),
')' => out.push(ExprToken::RParen),
',' => out.push(ExprToken::Comma),
_ => return Err(format!("unknown token '{c}'")),
}
}
}
out.push(ExprToken::Eof);
Ok(out)
}
#[derive(Debug, Clone)]
pub enum Ast {
Num(f64),
Var(String),
Unary(char, Box<Ast>),
Binary(char, Box<Ast>, Box<Ast>),
Call(String, Vec<Ast>),
}
impl Ast {
pub fn to_sexpr(&self) -> String {
match self {
Ast::Num(n) => format!("{n}"),
Ast::Var(v) => v.clone(),
Ast::Unary(op, e) => format!("({op} {})", e.to_sexpr()),
Ast::Binary(op, l, r) => format!("({op} {} {})", l.to_sexpr(), r.to_sexpr()),
Ast::Call(name, args) => {
let rendered: Vec<String> = args.iter().map(|a| a.to_sexpr()).collect();
format!("({name} {})", rendered.join(" "))
}
}
}
pub fn eval(&self) -> Result<f64, String> {
match self {
Ast::Num(n) => Ok(*n),
Ast::Var(v) => match v.as_str() {
"pi" => Ok(std::f64::consts::PI),
"e" => Ok(std::f64::consts::E),
_ => Err(format!("unknown constant '{v}'")),
},
Ast::Unary('-', inner) => Ok(-inner.eval()?),
Ast::Unary('+', inner) => inner.eval(),
Ast::Unary(op, _) => Err(format!("unsupported unary op '{op}'")),
Ast::Binary(op, l, r) => {
let a = l.eval()?;
let b = r.eval()?;
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' => Ok(a / b),
'^' => Ok(a.powf(b)),
_ => Err(format!("unsupported binary op '{op}'")),
}
}
Ast::Call(func, args) => {
let vals: Result<Vec<f64>, String> = args.iter().map(|a| a.eval()).collect();
let vals = vals?;
match (func.as_str(), vals.as_slice()) {
("sqrt", [x]) => Ok(x.sqrt()),
("max", [a, b]) => Ok(a.max(*b)),
("min", [a, b]) => Ok(a.min(*b)),
("abs", [x]) => Ok(x.abs()),
_ => Err(format!("unknown function call '{func}' with {} args", vals.len())),
}
}
}
}
}
pub struct PrattParser {
tokens: Vec<ExprToken>,
pos: usize,
}
impl PrattParser {
pub fn new(tokens: Vec<ExprToken>) -> Self {
Self { tokens, pos: 0 }
}
fn peek(&self) -> &ExprToken {
&self.tokens[self.pos]
}
fn next(&mut self) -> ExprToken {
let tok = self.tokens[self.pos].clone();
if self.pos + 1 < self.tokens.len() {
self.pos += 1;
}
tok
}
fn prefix_binding_power(op: char) -> u8 {
match op {
'+' | '-' => 7,
_ => panic!("not a prefix operator: {op}"),
}
}
fn infix_binding_power(op: char) -> Option<(u8, u8)> {
match op {
'+' | '-' => Some((3, 4)),
'*' | '/' => Some((5, 6)),
'^' => Some((8, 7)), // Right-associative: l_bp (8) > r_bp (7)
_ => None,
}
}
pub fn parse_expr(&mut self, min_bp: u8) -> Result<Ast, String> {
let mut lhs = match self.next() {
ExprToken::Number(n) => Ast::Num(n),
ExprToken::Ident(name) => {
if *self.peek() == ExprToken::LParen {
self.next(); // consume '('
let mut args = Vec::new();
if *self.peek() != ExprToken::RParen {
loop {
args.push(self.parse_expr(0)?);
if *self.peek() == ExprToken::Comma {
self.next();
} else {
break;
}
}
}
if self.next() != ExprToken::RParen {
return Err("expected ')' after function arguments".into());
}
Ast::Call(name, args)
} else {
Ast::Var(name)
}
}
ExprToken::LParen => {
let inner = self.parse_expr(0)?;
if self.next() != ExprToken::RParen {
return Err("expected closing ')'".into());
}
inner
}
ExprToken::Op(op @ ('+' | '-')) => {
let r_bp = Self::prefix_binding_power(op);
let rhs = self.parse_expr(r_bp)?;
Ast::Unary(op, Box::new(rhs))
}
other => return Err(format!("unexpected token in prefix position: {other:?}")),
};
while let ExprToken::Op(op) = *self.peek() {
let Some((l_bp, r_bp)) = Self::infix_binding_power(op) else {
break;
};
if l_bp < min_bp {
break;
}
self.next(); // consume operator
let rhs = self.parse_expr(r_bp)?;
lhs = Ast::Binary(op, Box::new(lhs), Box::new(rhs));
}
Ok(lhs)
}
}
fn main() {
let expressions = [
"1 + 2 * 3 - 4",
"2 ^ 3 ^ 2",
"-2 ^ 2 + max(10, sqrt(144) * 2)",
];
for src in expressions {
let tokens = lex_expr(src).unwrap();
let mut parser = PrattParser::new(tokens);
let ast = parser.parse_expr(0).unwrap();
println!("Input : {}", src);
println!("S-Expr: {}", ast.to_sexpr());
println!("Value : {}\n", ast.eval().unwrap());
}
}
Input : 1 + 2 * 3 - 4
S-Expr: (- (+ 1 (* 2 3)) 4)
Value : 3
Input : 2 ^ 3 ^ 2
S-Expr: (^ 2 (^ 3 2))
Value : 512
Input : -2 ^ 2 + max(10, sqrt(144) * 2)
S-Expr: (+ (- (^ 2 2)) (max 10 (* (sqrt 144) 2)))
Value : 20