Skip to content
← Back to Examples

Expression Evaluator

RustRubyexpression parsersintermediate

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 expr

Examples

1 + 2 * 3VALID

Arithmetic with precedence

2 ^ 3 ^ 2VALID

Right-associative exponent: 2^(3^2) = 512

x + yVALID

Variable expression

sin(PI / 2)VALID

Function call with constant

sqrt(16)VALID

Square root function

max(x, y)VALID

Multi-argument function

x > yVALID

Comparison expression

min(sin(0), cos(0))VALID

Nested function calls

Output Format

The parser returns an Expr enum that can be evaluated against an EvalContext containing variables and functions.

Structure

Expr

Enum: Number(f64), Variable(String), BinaryOp{left, op, right}, UnaryOp{op, operand}, FunctionCall{name, args}

EvalContext

Contains variables (HashMap<String, f64>) and functions (HashMap<String, fn>)

Visual Overview

Expression Evaluator diagram

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

Rust
//! 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(&current)?);
                }
                current.clear();
            }
            _ => current.push(c),
        }
    }

    if !current.trim().is_empty() {
        args.push(parse_expression(&current)?);
    }

    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);
            }
        }
    }
}
Ruby
# 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