Expression Evaluator
Parse and evaluate complex expressions with variables, function calls, and multiple operator types including comparison and mathematical functions.
Key Concepts
- operator precedence climbing
- variable resolution
- function dispatch
- context evaluation
- comparison operators
Why Parse This Format?
Full expression evaluators are needed for spreadsheet formulas, business rule engines, and domain-specific languages. They combine parsing with runtime evaluation against a context.
Use Cases
- Spreadsheet formula evaluation
- Business rule engine conditions
- Configuration value computation
- Template expression evaluation
- Interactive REPL environments
Input Format
Complex expressions with numbers, variables, function calls, and operators including arithmetic, comparison, and exponentiation. Supports standard math functions.
expr = number | variable | func(args) | expr op exprExamples
1 + 2 * 3VALIDArithmetic with precedence
2 ^ 3 ^ 2VALIDRight-associative exponent: 2^(3^2) = 512
x + yVALIDVariable expression
sin(PI / 2)VALIDFunction call with constant
sqrt(16)VALIDSquare root function
max(x, y)VALIDMulti-argument function
x > yVALIDComparison expression
min(sin(0), cos(0))VALIDNested function calls
Output Format
The parser returns an Expr enum that can be evaluated against an EvalContext containing variables and functions.
Structure
ExprEnum: Number(f64), Variable(String), BinaryOp{left, op, right}, UnaryOp{op, operand}, FunctionCall{name, args}
EvalContextContains variables (HashMap<String, f64>) and functions (HashMap<String, fn>)
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Number Parsing
.rule("number", re(r#"-?[0-9]+(\.[0-9]+)?"#))Numbers can be integers or floating-point: The regex handles optional sign prefix and decimal point. Both `42` and `-3.14` are valid.
Operator Precedence
.rule("factor", choice(vec![
dynamic(number),
dynamic(seq(vec![dynamic(str("(")), dynamic(expr), dynamic(str(")"))])),
]))
.rule("term", seq(vec![dynamic(factor), dynamic(term_tail).many()]))
.rule("expr", seq(vec![dynamic(term), dynamic(expr_tail).many()]))The grammar implements standard math precedence: `* /` > `+ -` Factors (numbers, parens) are combined into terms (mult/div), then terms into expressions (add/sub).
Multiplication and Division
.rule("term_tail", seq(vec![
dynamic(choice(vec![dynamic(str("*")), dynamic(str("/"))])),
dynamic(factor),
]))Terms chain factors with `*` and `/`: Left associativity ensures `a * b / c` parses as `(a * b) / c`.
Addition and Subtraction
.rule("expr_tail", seq(vec![
dynamic(choice(vec![dynamic(str("+")), dynamic(str("-"))])),
dynamic(term),
]))Expressions chain terms with `+` and `-`: Lower precedence means `a + b * c` parses as `a + (b * c)`.
Evaluation
fn evaluate(expr: &Expr) -> Result<f64, String> {
match expr {
Expr::Number(n) => Ok(n),
Expr::BinOp { left, op, right } => {
let l = evaluate(left)?;
let r = evaluate(right)?;
match op {
BinOp::Add => Ok(l + r),
BinOp::Sub => Ok(l - r),
BinOp::Mul => Ok(l * r),
BinOp::Div => Ok(l / r),
}
}
}
}The AST is evaluated recursively: Division by zero returns an error string.
Evaluation
fn evaluate(expr: &Expr) -> Result<f64, String> {
match expr {
Expr::Number(n) => Ok(n),
Expr::BinOp { left, op, right } => {
let l = evaluate(left)?;
let r = evaluate(right)?;
match op {
BinOp::Add => Ok(l + r),
BinOp::Sub => Ok(l - r),
BinOp::Mul => Ok(l * r),
BinOp::Div => Ok(l / r),
}
}
}
}The AST is evaluated recursively: Division by zero returns an error string.
Source Code
//! Expression Evaluator Example
//!
//! This example demonstrates building a complete expression parser and evaluator.
//! Shows operator precedence, variables, and function calls.
//!
//! Run with: cargo run --example expression_evaluator --no-default-features
#![allow(clippy::print_literal)]
#![allow(clippy::get_first)]
use parsanol::portable::{
infix::{Assoc, InfixBuilder},
parser_dsl::{choice, dynamic, re, ref_, seq, str, GrammarBuilder},
Grammar,
};
use std::collections::HashMap;
/// Build an expression grammar with full precedence
#[allow(dead_code)]
fn build_expression_grammar() -> Grammar {
let mut builder = GrammarBuilder::new();
// Basic atoms
builder = builder.rule("number", re(r"[0-9]+(\.[0-9]+)?"));
builder = builder.rule("identifier", re(r"[a-zA-Z_][a-zA-Z0-9_]*"));
// Function call: name(args)
builder = builder.rule(
"funcall",
seq(vec![
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(str("(")),
dynamic(re(r"[^)]*")), // simplified args
dynamic(str(")")),
]),
);
// Primary: number, function call, identifier, or parenthesized expression
builder = builder.rule(
"primary",
choice(vec![
dynamic(seq(vec![
dynamic(str("(")),
dynamic(ref_("expr")),
dynamic(str(")")),
])),
dynamic(seq(vec![
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(str("(")),
dynamic(re(r"[^)]*")),
dynamic(str(")")),
])),
dynamic(re(r"[0-9]+(\.[0-9]+)?")),
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
]),
);
// Build expression with operator precedence
// Higher precedence = binds tighter
let expr_atom = InfixBuilder::new()
.primary(ref_("primary"))
// Comparison (lowest precedence)
.op("==", 1, Assoc::NonAssoc)
.op("!=", 1, Assoc::NonAssoc)
.op("<", 1, Assoc::NonAssoc)
.op("<=", 1, Assoc::NonAssoc)
.op(">", 1, Assoc::NonAssoc)
.op(">=", 1, Assoc::NonAssoc)
// Addition/subtraction
.op("+", 2, Assoc::Left)
.op("-", 2, Assoc::Left)
// Multiplication/division/modulo
.op("*", 3, Assoc::Left)
.op("/", 3, Assoc::Left)
.op("%", 3, Assoc::Left)
// Exponentiation (right associative)
.op("^", 4, Assoc::Right)
// Unary not (highest)
.build(&mut builder);
builder.update_rule("expr", expr_atom);
builder.build()
}
/// Expression AST
#[derive(Debug, Clone)]
pub enum Expr {
Number(f64),
Variable(String),
BinaryOp {
left: Box<Expr>,
op: String,
right: Box<Expr>,
},
UnaryOp {
op: String,
operand: Box<Expr>,
},
FunctionCall {
name: String,
args: Vec<Expr>,
},
}
/// Evaluation context with variables and functions
#[derive(Debug, Clone, Default)]
pub struct EvalContext {
pub variables: HashMap<String, f64>,
pub functions: HashMap<String, fn(&[f64]) -> f64>,
}
impl EvalContext {
/// Create a new context with standard library
pub fn new() -> Self {
let mut ctx = Self::default();
// Standard functions
ctx.functions.insert("sin".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).sin()
});
ctx.functions.insert("cos".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).cos()
});
ctx.functions.insert("tan".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).tan()
});
ctx.functions.insert("sqrt".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).sqrt()
});
ctx.functions.insert("abs".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).abs()
});
ctx.functions.insert("floor".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).floor()
});
ctx.functions.insert("ceil".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).ceil()
});
ctx.functions.insert("round".to_string(), |args| {
args.get(0).copied().unwrap_or(0.0).round()
});
ctx.functions.insert("min".to_string(), |args| {
args.get(0)
.copied()
.unwrap_or(0.0)
.min(args.get(1).copied().unwrap_or(0.0))
});
ctx.functions.insert("max".to_string(), |args| {
args.get(0)
.copied()
.unwrap_or(0.0)
.max(args.get(1).copied().unwrap_or(0.0))
});
// Constants
ctx.variables.insert("PI".to_string(), std::f64::consts::PI);
ctx.variables.insert("E".to_string(), std::f64::consts::E);
ctx
}
/// Set a variable
pub fn set(&mut self, name: &str, value: f64) {
self.variables.insert(name.to_string(), value);
}
/// Evaluate an expression
pub fn eval(&self, expr: &Expr) -> Result<f64, String> {
match expr {
Expr::Number(n) => Ok(*n),
Expr::Variable(name) => self
.variables
.get(name)
.copied()
.ok_or_else(|| format!("Unknown variable: {}", name)),
Expr::BinaryOp { left, op, right } => {
let l = self.eval(left)?;
let r = self.eval(right)?;
match op.as_str() {
"+" => Ok(l + r),
"-" => Ok(l - r),
"*" => Ok(l * r),
"/" => Ok(l / r),
"%" => Ok(l % r),
"^" => Ok(l.powf(r)),
"==" => Ok(if l == r { 1.0 } else { 0.0 }),
"!=" => Ok(if l != r { 1.0 } else { 0.0 }),
"<" => Ok(if l < r { 1.0 } else { 0.0 }),
"<=" => Ok(if l <= r { 1.0 } else { 0.0 }),
">" => Ok(if l > r { 1.0 } else { 0.0 }),
">=" => Ok(if l >= r { 1.0 } else { 0.0 }),
_ => Err(format!("Unknown operator: {}", op)),
}
}
Expr::UnaryOp { op, operand } => {
let v = self.eval(operand)?;
match op.as_str() {
"-" => Ok(-v),
"!" => Ok(if v == 0.0 { 1.0 } else { 0.0 }),
_ => Err(format!("Unknown unary operator: {}", op)),
}
}
Expr::FunctionCall { name, args } => {
let arg_values: Result<Vec<f64>, _> = args.iter().map(|a| self.eval(a)).collect();
let arg_values = arg_values?;
if let Some(func) = self.functions.get(name) {
Ok(func(&arg_values))
} else {
Err(format!("Unknown function: {}", name))
}
}
}
}
}
/// Parse an expression string (simplified manual parsing)
pub fn parse_expression(input: &str) -> Result<Expr, String> {
let input = input.trim();
// Number
if let Ok(n) = input.parse::<f64>() {
return Ok(Expr::Number(n));
}
// Parenthesized expression
if input.starts_with('(') && input.ends_with(')') {
return parse_expression(&input[1..input.len() - 1]);
}
// Function call
if let Some(idx) = input.find('(') {
if input.ends_with(')') {
let name = &input[..idx];
let args_str = &input[idx + 1..input.len() - 1];
let args = parse_args(args_str)?;
return Ok(Expr::FunctionCall {
name: name.to_string(),
args,
});
}
}
// Binary operators (low to high precedence)
for op in [
"==", "!=", "<=", ">=", "<", ">", "+", "-", "*", "/", "%", "^",
] {
// Find rightmost occurrence for left-associative, leftmost for right-associative
let idx = if op == "^" {
input.find(op)
} else {
input.rfind(op)
};
if let Some(idx) = idx {
// Skip if inside parentheses
let before = &input[..idx];
let after = &input[idx + op.len()..];
// Check parentheses balance
let open = before.matches('(').count();
let close = before.matches(')').count();
if open != close {
continue;
}
if !before.is_empty() && !after.is_empty() {
let left = parse_expression(before)?;
let right = parse_expression(after)?;
return Ok(Expr::BinaryOp {
left: Box::new(left),
op: op.to_string(),
right: Box::new(right),
});
}
}
}
// Variable
if input.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Ok(Expr::Variable(input.to_string()));
}
Err(format!("Could not parse: {}", input))
}
/// Parse comma-separated arguments
fn parse_args(input: &str) -> Result<Vec<Expr>, String> {
if input.trim().is_empty() {
return Ok(Vec::new());
}
let mut args = Vec::new();
let mut current = String::new();
let mut depth = 0;
for c in input.chars() {
match c {
'(' => {
depth += 1;
current.push(c);
}
')' => {
depth -= 1;
current.push(c);
}
',' if depth == 0 => {
if !current.trim().is_empty() {
args.push(parse_expression(¤t)?);
}
current.clear();
}
_ => current.push(c),
}
}
if !current.trim().is_empty() {
args.push(parse_expression(¤t)?);
}
Ok(args)
}
fn main() {
println!("Expression Evaluator Example");
println!("============================\n");
let mut ctx = EvalContext::new();
ctx.set("x", 10.0);
ctx.set("y", 5.0);
let expressions = [
"1 + 2 * 3",
"(1 + 2) * 3",
"2 ^ 3 ^ 2", // Right associative: 2^(3^2) = 2^9 = 512
"x + y",
"x * y - 5",
"sin(PI / 2)",
"sqrt(16)",
"max(x, y)",
"x > y",
"min(sin(0), cos(0))",
];
println!(
"Variables: x = {}, y = {}",
ctx.variables.get("x").unwrap(),
ctx.variables.get("y").unwrap()
);
println!(
"Constants: PI = {}, E = {}\n",
std::f64::consts::PI,
std::f64::consts::E
);
println!("{:<25} | {:<15} | {}", "Expression", "Result", "AST");
println!("{}", "-".repeat(70));
for expr_str in expressions {
match parse_expression(expr_str) {
Ok(expr) => match ctx.eval(&expr) {
Ok(result) => {
println!("{:<25} | {:<15.4} | {:?}", expr_str, result, expr);
}
Err(e) => {
println!("{:<25} | Error: {}", expr_str, e);
}
},
Err(e) => {
println!("{:<25} | Parse Error: {}", expr_str, e);
}
}
}
}
# frozen_string_literal: true
# Expression Evaluator - Ruby Implementation
#
# A complete expression parser with operator precedence, variables,
# and function calls. Demonstrates building a practical calculator.
#
# Run with: ruby example/expression-evaluator/basic.rb
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
# Expression parser with full operator precedence
class ExpressionParser < Parsanol::Parser
root :expression
# Comparison (lowest precedence)
rule(:expression) { comparison }
rule(:comparison) do
(addition.as(:left) >>
((match("==|!=|<=|>=|<>").as(:op) | str("<") | str(">")).as(:op) >>
addition.as(:right)).repeat(1)) |
addition
end
# Addition/subtraction
rule(:addition) do
(multiplication.as(:left) >>
(match("[+-]").as(:op) >> multiplication.as(:right)).repeat(1)) |
multiplication
end
# Multiplication/division/modulo
rule(:multiplication) do
(power.as(:left) >>
(match("[*/%]").as(:op) >> power.as(:right)).repeat(1)) |
power
end
# Exponentiation (right associative)
rule(:power) do
(unary.as(:left) >>
(str("^").as(:op) >> power.as(:right)).maybe) |
unary
end
# Unary operators
rule(:unary) do
(str("-").as(:op) >> unary.as(:operand)).as(:unary) |
(str("!").as(:op) >> unary.as(:operand)).as(:unary) |
primary
end
# Primary: number, function call, variable, or parenthesized expression
rule(:primary) do
funcall |
number |
variable |
(lparen >> expression >> rparen)
end
rule(:number) do
((match("[0-9]").repeat(1) >> str(".") >> match("[0-9]").repeat(1)) |
match("[0-9]").repeat(1)).as(:number) >> space?
end
rule(:variable) do
(match("[a-zA-Z_]") >> match("[a-zA-Z0-9_]").repeat).as(:variable) >> space?
end
rule(:funcall) do
(match("[a-zA-Z_]") >> match("[a-zA-Z0-9_]").repeat).as(:name) >>
lparen >>
arglist.as(:args) >>
rparen
end
rule(:arglist) do
(expression >> (comma >> expression).repeat).maybe
end
rule(:lparen) { str("(") >> space? }
rule(:rparen) { str(")") >> space? }
rule(:comma) { str(",") >> space? }
rule(:space?) { match('\s').repeat }
end
# AST node classes
Number = Struct.new(:value) do
def eval(_ctx)
value
end
end
Variable = Struct.new(:name) do
def eval(ctx)
ctx.variables.fetch(name) { raise "Unknown variable: #{name}" }
end
end
BinaryOp = Struct.new(:left, :op, :right) do
def eval(ctx)
l = left.eval(ctx)
r = right.eval(ctx)
case op
when "+" then l + r
when "-" then l - r
when "*" then l * r
when "/" then l / r
when "%" then l % r
when "^" then l**r
when "==" then l == r ? 1.0 : 0.0
when "!=" then l == r ? 0.0 : 1.0
when "<" then l < r ? 1.0 : 0.0
when ">" then l > r ? 1.0 : 0.0
when "<=" then l <= r ? 1.0 : 0.0
when ">=" then l >= r ? 1.0 : 0.0
else raise "Unknown operator: #{op}"
end
end
end
UnaryOp = Struct.new(:op, :operand) do
def eval(ctx)
v = operand.eval(ctx)
case op
when "-" then -v
when "!" then v.zero? ? 1.0 : 0.0
else raise "Unknown unary operator: #{op}"
end
end
end
FunctionCall = Struct.new(:name, :args) do
def eval(ctx)
arg_values = args.map { |a| a.eval(ctx) }
raise "Unknown function: #{name}" unless ctx.functions.key?(name)
ctx.functions[name].call(arg_values)
end
end
# Transform parse tree to AST
class ExpressionTransform < Parsanol::Transform
rule(number: simple(:n)) { Number.new(n.to_s.to_f) }
rule(variable: simple(:v)) { Variable.new(v.to_s) }
rule(name: simple(:n), args: simple(:a)) do
FunctionCall.new(n.to_s, a.is_a?(Array) ? a : [a])
end
rule(name: simple(:n), args: sequence(:a)) do
FunctionCall.new(n.to_s, a)
end
rule(left: simple(:l)) { l }
rule(left: simple(:l), op: simple(:o), right: simple(:r)) do
BinaryOp.new(l, o.to_s, r)
end
rule(unary: { op: simple(:o), operand: simple(:e) }) do
UnaryOp.new(o.to_s, e)
end
end
# Evaluation context with variables and functions
class EvalContext
attr_accessor :variables, :functions
def initialize
@variables = {
"PI" => Math::PI,
"E" => Math::E,
}
@functions = {
"sin" => ->(args) { Math.sin(args[0] || 0) },
"cos" => ->(args) { Math.cos(args[0] || 0) },
"tan" => ->(args) { Math.tan(args[0] || 0) },
"sqrt" => ->(args) { Math.sqrt(args[0] || 0) },
"abs" => ->(args) { (args[0] || 0).abs },
"floor" => ->(args) { (args[0] || 0).floor },
"ceil" => ->(args) { (args[0] || 0).ceil },
"round" => ->(args) { (args[0] || 0).round },
"min" => ->(args) { [args[0] || 0, args[1] || 0].min },
"max" => ->(args) { [args[0] || 0, args[1] || 0].max },
"log" => ->(args) { Math.log(args[0] || 1) },
"exp" => ->(args) { Math.exp(args[0] || 0) },
}
end
def set(name, value)
@variables[name] = value
end
end
# Evaluate an expression string
def evaluate(str, ctx = EvalContext.new)
parser = ExpressionParser.new
transform = ExpressionTransform.new
tree = parser.parse(str)
ast = transform.apply(tree)
# Handle BinaryOp chains with multiple ops
ast = ast.first if ast.is_a?(Array) && ast.length == 1
# Reduce left-associative chains
while ast.is_a?(Hash) && ast.key?(:left)
left = ast[:left]
if left.is_a?(Hash) && left.key?(:left)
# Nested chain - flatten
inner = evaluate_helper(left, ctx)
ast = BinaryOp.new(inner, ast[:op], ast[:right])
else
ast = BinaryOp.new(left, ast[:op], ast[:right])
end
end
ast.eval(ctx)
rescue StandardError => e
"Error: #{e.message}"
end
def evaluate_helper(node, ctx)
return node unless node.is_a?(Hash)
if node.key?(:left)
left = evaluate_helper(node[:left], ctx)
right = evaluate_helper(node[:right], ctx)
BinaryOp.new(left, node[:op], right)
else
node
end
end
# Main demo
if __FILE__ == $PROGRAM_NAME
ctx = EvalContext.new
ctx.set("x", 10.0)
ctx.set("y", 5.0)
puts "Expression Evaluator Example"
puts "=" * 40
puts
puts "Variables: x = #{ctx.variables['x']}, y = #{ctx.variables['y']}"
puts "Constants: PI = #{ctx.variables['PI']}, E = #{ctx.variables['E']}"
puts
expressions = [
"1 + 2 * 3",
"(1 + 2) * 3",
"2 ^ 3 ^ 2",
"x + y",
"x * y - 5",
"sin(PI / 2)",
"sqrt(16)",
"max(x, y)",
"x > y",
"min(sin(0), cos(0))",
]
printf "%-25s | %s\n", "Expression", "Result"
puts "-" * 40
expressions.each do |expr|
result = evaluate(expr, ctx)
printf "%-25s | %s\n", expr, result
end
# Command line argument
if ARGV.length.positive?
expr = ARGV.join(" ")
puts
puts "Evaluating: #{expr}"
puts "Result: #{evaluate(expr, ctx)}"
end
end