Skip to content
← Back to Examples

Boolean Algebra Parser

RustRubyexpression parsersintermediate

Parse boolean expressions with AND/OR operators, supporting proper precedence, parentheses, and variable evaluation.

Key Concepts

  • operator precedence
  • variable binding
  • truth evaluation
  • DNF conversion
  • recursive structure

Why Parse This Format?

Boolean expressions are fundamental to search queries, conditional logic, circuit design, and formal verification. Parsing them correctly enables powerful query and rule systems.

Use Cases

  • Search engine query parsing
  • Rule engine condition evaluation
  • Digital circuit simulation
  • Database query predicates
  • Access control rule evaluation

Input Format

Boolean expressions using variables (varN), AND, and OR operators. AND has higher precedence than OR. Parentheses can override precedence.

expression = variable | (expression) | expression AND expression | expression OR expression

Examples

var1VALID

Simple variable

var1 and var2VALID

AND expression

var1 or var2VALID

OR expression

var1 and var2 or var3VALID

Mixed precedence: (var1 AND var2) OR var3

var1 or var2 and var3VALID

AND binds tighter: var1 OR (var2 AND var3)

(var1 or var2) and var3VALID

Parentheses override precedence

var1 and var2 and var3VALID

Chained AND

var1 & var2INVALID

Invalid: wrong operator syntax

Output Format

The parser returns a BoolExpr enum representing the expression tree that can be evaluated with variable bindings.

Structure

type

BoolExpr enum with variants: Var(String), And(Box<BoolExpr>, Box<BoolExpr>), Or(Box<BoolExpr>, Box<BoolExpr>)

evaluation

Can be evaluated with HashMap<String, bool> bindings

Visual Overview

Boolean Algebra Parser diagram

How the Code Works

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

Variable Parsing

.rule("variable", re("var[0-9]+"))

Boolean variables follow a simple pattern: Variables are named `var` followed by digits (e.g., `var1`, `var42`). This allows for easy testing with predictable names.

Operator Precedence

.rule("primary", choice(vec![
    dynamic(variable),
    dynamic(seq(vec![dynamic(str("(")), dynamic(expr), dynamic(str(")"))])),
    dynamic(seq(vec![dynamic(str("NOT")), dynamic(primary)])),
]))
.rule("and_expr", seq(vec![dynamic(primary), dynamic(and_tail).many()]))
.rule("or_expr", seq(vec![dynamic(and_expr), dynamic(or_tail).many()]))

The grammar implements proper precedence: NOT > AND > OR NOT binds tightest (prefix), then AND (infix), then OR (lowest precedence).

AND Expression Parsing

.rule("and_tail", seq(vec![
    dynamic(str(" AND ")),
    dynamic(primary),
]))

AND expressions chain primary values: The `and_tail` is repeated zero or more times, creating left-associative chains like `var1 AND var2 AND var3`.

OR Expression Parsing

.rule("or_tail", seq(vec![
    dynamic(str(" OR ")),
    dynamic(and_expr),
]))

OR expressions chain AND expressions: OR has lowest precedence, so `var1 OR var2 AND var3` parses as `var1 OR (var2 AND var3)`.

Evaluation

fn evaluate(expr: &BoolExpr, vars: &HashMap<String, bool>) -> bool {
    match expr {
        BoolExpr::Var(name) => vars.get(name).copied().unwrap_or(false),
        BoolExpr::And(left, right) => evaluate(left, vars) && evaluate(right, vars),
        BoolExpr::Or(left, right) => evaluate(left, vars) || evaluate(right, vars),
        BoolExpr::Not(inner) => !evaluate(inner, vars),
    }
}

The evaluation context maps variables to boolean values: Variables must looked up in the context HashMap, defaulting to false.

Evaluation

fn evaluate(expr: &BoolExpr, vars: &HashMap<String, bool>) -> bool {
    match expr {
        BoolExpr::Var(name) => vars.get(name).copied().unwrap_or(false),
        BoolExpr::And(left, right) => evaluate(left, vars) && evaluate(right, vars),
        BoolExpr::Or(left, right) => evaluate(left, vars) || evaluate(right, vars),
        BoolExpr::Not(inner) => !evaluate(inner, vars),
    }
}

The evaluation context maps variables to boolean values: Variables must looked up in the context HashMap, defaulting to false.

Source Code

Rust
//! Boolean Algebra Parser Example
//!
//! This example demonstrates parsing boolean expressions with AND/OR operators.
//! Shows operator precedence, parentheses handling, and DNF conversion.
//! Based on the Parslet boolean_algebra.rb example.
//!
//! Run with: cargo run --example boolean_algebra --no-default-features

#![allow(clippy::print_literal)]

use parsanol::portable::{
    parser_dsl::{choice, dynamic, re, ref_, seq, str, GrammarBuilder},
    AstArena, Grammar, PortableParser,
};

/// Build a boolean algebra grammar
fn build_boolean_grammar() -> Grammar {
    let mut builder = GrammarBuilder::new();

    // Basic tokens
    builder = builder.rule("space", re("[ \\t]+"));
    builder = builder.rule("lparen", str("("));
    builder = builder.rule("rparen", str(")"));

    // Operators
    builder = builder.rule("and_op", str("and"));
    builder = builder.rule("or_op", str("or"));

    // Variable: var followed by digits
    builder = builder.rule("variable", re("var[0-9]+"));

    // Primary: parenthesized expression or variable
    builder = builder.rule(
        "primary",
        choice(vec![
            dynamic(seq(vec![
                dynamic(str("(")),
                dynamic(re("[ \\t]*")),
                dynamic(ref_("or_expr")),
                dynamic(re("[ \\t]*")),
                dynamic(str(")")),
            ])),
            dynamic(re("var[0-9]+")),
        ]),
    );

    // AND expression: primary (AND primary)*
    builder = builder.rule(
        "and_expr",
        seq(vec![
            dynamic(re("var[0-9]+")),
            dynamic(re("(?:[ \\t]+and[ \\t]+var[0-9]+)*")),
        ]),
    );

    // OR expression: and_expr (OR and_expr)*
    builder = builder.rule(
        "or_expr",
        seq(vec![
            dynamic(re("var[0-9]+(?:[ \\t]+and[ \\t]+var[0-9]+)*")),
            dynamic(re(
                "(?:[ \\t]+or[ \\t]+var[0-9]+(?:[ \\t]+and[ \\t]+var[0-9]+)*)*",
            )),
        ]),
    );

    builder.build()
}

/// Boolean expression AST
#[derive(Debug, Clone)]
pub enum BoolExpr {
    Var(String),
    And(Box<BoolExpr>, Box<BoolExpr>),
    Or(Box<BoolExpr>, Box<BoolExpr>),
}

impl std::fmt::Display for BoolExpr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BoolExpr::Var(v) => write!(f, "{}", v),
            BoolExpr::And(l, r) => write!(f, "({} AND {})", l, r),
            BoolExpr::Or(l, r) => write!(f, "({} OR {})", l, r),
        }
    }
}

/// Parse a boolean expression string
pub fn parse_boolean(input: &str) -> Result<BoolExpr, String> {
    let input = input.trim();
    let grammar = build_boolean_grammar();
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);

    parser
        .parse()
        .map_err(|e| format!("Parse error: {:?}", e))?;

    // Manual parsing for actual AST construction
    parse_or_expr(input)
}

/// Parse OR expression (lowest precedence)
fn parse_or_expr(input: &str) -> Result<BoolExpr, String> {
    let input = input.trim();

    // Find OR at the top level (not inside parentheses)
    let mut depth = 0;
    let mut or_pos = None;

    for (i, c) in input.char_indices() {
        match c {
            '(' => depth += 1,
            ')' => depth -= 1,
            _ if depth == 0 && input[i..].starts_with(" or ") => or_pos = Some(i),
            _ => {}
        }
    }

    if let Some(pos) = or_pos {
        let left = parse_and_expr(&input[..pos])?;
        let right = parse_or_expr(&input[pos + 4..])?;
        return Ok(BoolExpr::Or(Box::new(left), Box::new(right)));
    }

    parse_and_expr(input)
}

/// Parse AND expression (higher precedence than OR)
fn parse_and_expr(input: &str) -> Result<BoolExpr, String> {
    let input = input.trim();

    // Find AND at the top level
    let mut depth = 0;
    let mut and_pos = None;

    for (i, c) in input.char_indices() {
        match c {
            '(' => depth += 1,
            ')' => depth -= 1,
            _ if depth == 0 && input[i..].starts_with(" and ") => and_pos = Some(i),
            _ => {}
        }
    }

    if let Some(pos) = and_pos {
        let left = parse_primary(&input[..pos])?;
        let right = parse_and_expr(&input[pos + 5..])?;
        return Ok(BoolExpr::And(Box::new(left), Box::new(right)));
    }

    parse_primary(input)
}

/// Parse primary (variable or parenthesized expression)
fn parse_primary(input: &str) -> Result<BoolExpr, String> {
    let input = input.trim();

    // Parenthesized expression
    if input.starts_with('(') && input.ends_with(')') {
        return parse_or_expr(&input[1..input.len() - 1]);
    }

    // Variable
    if input.starts_with("var") && input.len() > 3 {
        let num_part = &input[3..];
        if num_part.chars().all(|c| c.is_ascii_digit()) {
            return Ok(BoolExpr::Var(input.to_string()));
        }
    }

    Err(format!("Invalid primary: {}", input))
}

/// Evaluate boolean expression with variable bindings
pub fn eval(
    expr: &BoolExpr,
    bindings: &std::collections::HashMap<String, bool>,
) -> Result<bool, String> {
    match expr {
        BoolExpr::Var(v) => bindings
            .get(v)
            .copied()
            .ok_or_else(|| format!("Unknown variable: {}", v)),
        BoolExpr::And(l, r) => Ok(eval(l, bindings)? && eval(r, bindings)?),
        BoolExpr::Or(l, r) => Ok(eval(l, bindings)? || eval(r, bindings)?),
    }
}

fn main() {
    println!("Boolean Algebra Parser Example");
    println!("==============================\n");

    let expressions = [
        "var1",
        "var1 and var2",
        "var1 or var2",
        "var1 and var2 or var3",
        "var1 or var2 and var3",
        "(var1 or var2) and var3",
        "var1 and (var2 or var3)",
        "var1 and var2 and var3",
    ];

    println!("{:<35} | {}", "Expression", "Parsed AST");
    println!("{}", "-".repeat(70));

    for expr_str in expressions {
        match parse_boolean(expr_str) {
            Ok(expr) => {
                println!("{:<35} | {}", expr_str, expr);
            }
            Err(e) => {
                println!("{:<35} | ERROR: {}", expr_str, e);
            }
        }
    }

    // Demonstrate evaluation
    println!("\nEvaluation Example:");
    println!("-------------------");

    use std::collections::HashMap;
    let mut bindings = HashMap::new();
    bindings.insert("var1".to_string(), true);
    bindings.insert("var2".to_string(), false);
    bindings.insert("var3".to_string(), true);

    println!("Bindings: var1=true, var2=false, var3=true\n");

    let eval_exprs = [
        "var1 and var2",
        "var1 or var2",
        "var1 and var3",
        "(var1 or var2) and var3",
    ];

    for expr_str in eval_exprs {
        if let Ok(expr) = parse_boolean(expr_str) {
            if let Ok(result) = eval(&expr, &bindings) {
                println!("  {} = {}", expr_str, result);
            }
        }
    }
}
Ruby
# frozen_string_literal: true

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

require "parsanol/parslet"
require "pp"

# Parses strings like "var1 and (var2 or var3)" respecting operator precedence
# and parentheses. After that transforms the parse tree into an array of
# arrays like this:
#
# [["1", "2"], ["1", "3"]]
#
# The array represents a DNF (disjunctive normal form). Elements of outer
# array are connected with "or" operator, while elements of inner arrays are
# joined with "and".
#
class MyParser < Parsanol::Parser
  rule(:space)  { match[" "].repeat(1) }
  rule(:space?) { space.maybe }

  rule(:lparen) { str("(") >> space? }
  rule(:rparen) { str(")") >> space? }

  rule(:and_operator) { str("and") >> space? }
  rule(:or_operator)  { str("or")  >> space? }

  rule(:var) { str("var") >> match["0-9"].repeat(1).as(:var) >> space? }

  # The primary rule deals with parentheses.
  rule(:primary) { (lparen >> or_operation >> rparen) | var }

  # Note that following rules are both right-recursive.
  rule(:and_operation) do
    (primary.as(:left) >> and_operator >>
      and_operation.as(:right)).as(:and) |
      primary
  end

  rule(:or_operation) do
    (and_operation.as(:left) >> or_operator >>
      or_operation.as(:right)).as(:or) |
      and_operation
  end

  # We start at the lowest precedence rule.
  root(:or_operation)
end

class Transformer < Parsanol::Transform
  rule(var: simple(:var)) { [[String(var)]] }

  rule(or: { left: subtree(:left), right: subtree(:right) }) do
    left + right
  end

  rule(and: { left: subtree(:left), right: subtree(:right) }) do
    res = []
    left.each do |l|
      right.each do |r|
        res << (l + r)
      end
    end
    res
  end
end

pp tree = MyParser.new.parse("var1 and (var2 or var3)")
# {:and=>
#   {:left=>{:var=>"1"@3},
#    :right=>{:or=>{:left=>{:var=>"2"@13}, :right=>{:var=>"3"@21}}}}}
pp Transformer.new.apply(tree)
# [["1", "2"], ["1", "3"]]