Skip to content
← Back to Examples

Precedence Calculator

RustRubyexpression parsersintermediate

Parse and evaluate mathematical expressions using precedence climbing for operator precedence handling.

Key Concepts

  • precedence climbing
  • left vs right associativity
  • variable assignment
  • expression evaluation

Why Parse This Format?

Precedence climbing is a classic algorithm for parsing infix expressions with proper operator precedence.

Use Cases

  • Calculator applications
  • Expression evaluation in spreadsheets
  • Query language parsers
  • Configuration file expressions

Input Format

Variable assignments with mathematical expressions, one per line.

identifier = expression

Examples

a = 1 + 2 * 3VALID

Assignment with precedence (multiplication first)

b = 10 - 3 - 2VALID

Left-associative subtraction

c = 100VALID

Simple integer assignment

Output Format

Variable bindings mapping names to evaluated values.

Structure

bindings

Map of variable names to integer values

Visual Overview

Precedence Calculator diagram

How the Code Works

This section explains how the parsing rules map to the format semantics.

Grammar Construction

let mut builder = GrammarBuilder::new();

builder = builder.rule("digit", re(r"[0-9]"));
builder = builder.rule("integer", re(r"[0-9]+"));
builder = builder.rule("identifier", re(r"[a-z][a-zA-Z0-9]*"));

The grammar is built using the GrammarBuilder DSL: Basic rules define atoms: digits, integers, and identifiers.

InfixBuilder for Precedence

let expr_atom = InfixBuilder::new()
    .primary(ref_("primary"))
    .op("*", 2, Assoc::Left)
    .op("/", 2, Assoc::Left)
    .op("+", 1, Assoc::Right)
    .op("-", 1, Assoc::Right)
    .build(&mut builder);

The InfixBuilder handles operator precedence automatically: Higher precedence numbers bind tighter. Multiplication (2) binds before addition (1).

Association Types

Assoc::Left   // 10 - 3 - 2 = (10 - 3) - 2 = 5
Assoc::Right  // 2 ^ 3 ^ 2 = 2 ^ (3 ^ 2) = 512

Left and right associativity are specified: Left-associative operators group left-to-right; right-associative group right-to-left.

Expression AST

pub enum Expr {
    Integer(i64),
    BinaryOp {
        left: Box<Expr>,
        op: String,
        right: Box<Expr>,
    },
}

The AST represents binary operations recursively: Box enables recursive structure; operation strings allow multiple operators.

Variable Bindings

pub struct Bindings {
    vars: HashMap<String, i64>,
}

A simple HashMap stores variable values: Assignments add to the map; lookups read from it.

Evaluation

fn eval(expr: &Expr) -> i64 {
    match expr {
        Expr::Integer(n) => *n,
        Expr::BinaryOp { left, op, right } => {
            let l = eval(left);
            let r = eval(right);
            match op.as_str() {
                "+" => l + r,
                "-" => l - r,
                "*" => l * r,
                "/" => l / r,
                _ => panic!("Unknown operator"),
            }
        }
    }
}

Recursive evaluation computes expression values: Post-order traversal evaluates children before applying operator.

Evaluation

fn eval(expr: &Expr) -> i64 {
    match expr {
        Expr::Integer(n) => *n,
        Expr::BinaryOp { left, op, right } => {
            let l = eval(left);
            let r = eval(right);
            match op.as_str() {
                "+" => l + r,
                "-" => l - r,
                "*" => l * r,
                "/" => l / r,
                _ => panic!("Unknown operator"),
            }
        }
    }
}

Recursive evaluation computes expression values: Post-order traversal evaluates children before applying operator.

Source Code

Rust
//! Precedence Calculator Example
//!
//! Demonstrates precedence climbing for infix expression parsing.
//! Shows how to parse variable assignments with proper operator precedence.
//!
//! Run with: cargo run --example prec-calc --no-default-features

#![allow(clippy::print_literal)]

use std::collections::HashMap;

/// Expression AST
#[derive(Debug, Clone)]
pub enum Expr {
    Integer(i64),
    BinaryOp {
        left: Box<Expr>,
        op: String,
        right: Box<Expr>,
    },
}

/// Variable bindings
#[derive(Debug, Clone, Default)]
pub struct Bindings {
    vars: HashMap<String, i64>,
}

impl Bindings {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn set(&mut self, name: &str, value: i64) {
        self.vars.insert(name.to_string(), value);
    }

    pub fn get(&self, name: &str) -> Option<i64> {
        self.vars.get(name).copied()
    }
}

/// Evaluate an expression
fn eval(expr: &Expr) -> i64 {
    match expr {
        Expr::Integer(n) => *n,
        Expr::BinaryOp { left, op, right } => {
            let l = eval(left);
            let r = eval(right);
            match op.as_str() {
                "+" => l + r,
                "-" => l - r,
                "*" => l * r,
                "/" => l / r,
                _ => panic!("Unknown operator: {}", op),
            }
        }
    }
}

/// Parse and evaluate a simple expression (manual parsing for demo)
fn parse_expression(input: &str) -> Result<Expr, String> {
    let input = input.trim();

    // Try to parse as integer first
    if let Ok(n) = input.parse::<i64>() {
        return Ok(Expr::Integer(n));
    }

    // Binary operators (low to high precedence)
    for op in ["+", "-", "*", "/"] {
        // Find rightmost for left-assoc, leftmost for right-assoc
        let idx = if op == "+" {
            input.rfind(op)
        } else {
            input.find(op).or_else(|| input.rfind(op))
        };

        if let Some(idx) = idx {
            let before = &input[..idx];
            let after = &input[idx + 1..];

            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),
                });
            }
        }
    }

    Err(format!("Could not parse: {}", input))
}

/// Parse and evaluate assignments
fn parse_assignments(input: &str) -> Bindings {
    let mut bindings = Bindings::new();

    for line in input.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        if let Some(eq_pos) = line.find('=') {
            let name = line[..eq_pos].trim();
            let expr_str = line[eq_pos + 1..].trim();

            match parse_expression(expr_str) {
                Ok(expr) => {
                    let value = eval(&expr);
                    bindings.set(name, value);
                    println!("  {} = {} (from {})", name, value, expr_str);
                }
                Err(e) => {
                    println!("  Error parsing '{}': {}", expr_str, e);
                }
            }
        }
    }

    bindings
}

fn main() {
    println!("Precedence Calculator Example");
    println!("==============================");
    println!();

    let input = r#"
a = 1
b = 2
c = 3 * 25
d = 100 + 3*4
e = 10 - 3 - 2
"#;

    println!("Input:");
    println!("{}", input);
    println!();

    println!("Parsing and evaluating:");
    let bindings = parse_assignments(input);

    println!();
    println!("Final bindings:");
    for (name, value) in bindings.vars.iter() {
        println!("  {} = {}", name, value);
    }

    // Demonstrate precedence
    println!();
    println!("Precedence demonstration:");
    let test_exprs = [
        ("1 + 2 * 3", 7),     // 1 + (2 * 3) = 7
        ("2 * 3 + 4", 10),    // (2 * 3) + 4 = 10
        ("10 - 3 - 2", 5),    // (10 - 3) - 2 = 5 (left assoc)
        ("100 + 3 * 4", 112), // 100 + (3 * 4) = 112
    ];

    for (expr_str, expected) in test_exprs {
        match parse_expression(expr_str) {
            Ok(expr) => {
                let result = eval(&expr);
                let status = if result == expected { "OK" } else { "FAIL" };
                println!(
                    "  {} = {} (expected {}) {}",
                    expr_str, result, expected, status
                );
            }
            Err(e) => {
                println!("  {} -> Error: {}", expr_str, e);
            }
        }
    }
}
Ruby
# frozen_string_literal: true

# A demonstration of the new precedence climbing infix expression parser.

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"

require "pp"
require "rspec"
require "parsanol/parslet"
require "parsanol/rig/rspec"
require "parsanol/convenience"

class InfixExpressionParser < Parsanol::Parser
  root :variable_assignment_list

  rule(:space) { match[" "] }

  def cts(atom)
    atom >> space.repeat
  end

  def infix(*)
    Infix.new(*)
  end

  # This is the heart of the infix expression parser: real simple definitions
  # for all the pieces we need.
  rule(:mul_op) { cts match["*/"] }
  rule(:add_op) { cts match["+-"] }
  rule(:digit) { match["0-9"] }
  rule(:integer) { cts digit.repeat(1).as(:int) }

  rule(:expression) do
    infix_expression(integer,
                     [mul_op, 2, :left],
                     [add_op, 1, :right])
  end

  # And now adding variable assignments to that, just to a) demonstrate this
  # embedded in a bigger parser, and b) make the example interesting.
  rule(:variable_assignment_list) do
    variable_assignment.repeat(1)
  end
  rule(:variable_assignment) do
    identifier.as(:ident) >> equal_sign >> expression.as(:exp) >> eol
  end
  rule(:identifier) do
    cts(match["a-z"] >> match["a-zA-Z0-9"].repeat)
  end
  rule(:equal_sign) do
    cts str("=")
  end
  rule(:eol) do
    cts(str("\n")) | any.absent?
  end
end

class InfixInterpreter < Parsanol::Transform
  rule(int: simple(:int)) { Integer(int) }
  rule(ident: simple(:ident), exp: simple(:result)) do |d|
    d[:doc][d[:ident].to_s.strip.to_sym] = d[:result]
  end

  rule(l: simple(:l), o: /^\*/, r: simple(:r)) { l * r }
  rule(l: simple(:l), o: /^\+/, r: simple(:r)) { l + r }
end

input = <<~ASSIGNMENTS
  a = 1
  b = 2
  c = 3 * 25
  d = 100 + 3*4
ASSIGNMENTS

puts input

int_tree = InfixExpressionParser.new.parse_with_debug(input)
bindings = {}
InfixInterpreter.new.apply(int_tree, doc: bindings)

pp bindings