logicaffeine-language 0.10.1

Natural language to first-order logic pipeline
Documentation
//! AST visitor pattern for traversing logical expressions.
//!
//! This module provides a visitor trait for walking the AST without mutation.
//! It follows the standard visitor pattern with `walk_*` functions that handle
//! recursive traversal and `visit_*` methods that can be overridden.
//!
//! # Usage
//!
//! Implement [`Visitor`] and override the `visit_*` methods you need:
//!
//! ```no_run
//! # use logicaffeine_base::Symbol;
//! # use logicaffeine_language::ast::Term;
//! # use logicaffeine_language::visitor::{Visitor, walk_term};
//! struct VariableCollector {
//!     vars: Vec<Symbol>,
//! }
//!
//! impl<'a> Visitor<'a> for VariableCollector {
//!     fn visit_term(&mut self, term: &'a Term<'a>) {
//!         if let Term::Variable(sym) = term {
//!             self.vars.push(*sym);
//!         }
//!         walk_term(self, term);
//!     }
//! }
//! ```

use crate::ast::{LogicExpr, NounPhrase, Term};

/// Trait for visiting AST nodes.
pub trait Visitor<'a>: Sized {
    fn visit_expr(&mut self, expr: &'a LogicExpr<'a>) {
        walk_expr(self, expr);
    }

    fn visit_term(&mut self, term: &'a Term<'a>) {
        walk_term(self, term);
    }

    fn visit_np(&mut self, np: &'a NounPhrase<'a>) {
        walk_np(self, np);
    }
}

pub fn walk_expr<'a, V: Visitor<'a>>(v: &mut V, expr: &'a LogicExpr<'a>) {
    match expr {
        LogicExpr::Predicate { args, .. } => {
            for arg in *args {
                v.visit_term(arg);
            }
        }

        LogicExpr::Identity { left, right } => {
            v.visit_term(left);
            v.visit_term(right);
        }

        LogicExpr::Metaphor { tenor, vehicle } => {
            v.visit_term(tenor);
            v.visit_term(vehicle);
        }

        LogicExpr::Quantifier { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::Categorical(data) => {
            v.visit_np(&data.subject);
            v.visit_np(&data.predicate);
        }

        LogicExpr::Relation(data) => {
            v.visit_np(&data.subject);
            v.visit_np(&data.object);
        }

        LogicExpr::Modal { operand, .. } => {
            v.visit_expr(operand);
        }

        LogicExpr::Temporal { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::TemporalBinary { left, right, .. } => {
            v.visit_expr(left);
            v.visit_expr(right);
        }

        LogicExpr::Aspectual { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::Voice { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::BinaryOp { left, right, .. } => {
            v.visit_expr(left);
            v.visit_expr(right);
        }

        LogicExpr::UnaryOp { operand, .. } => {
            v.visit_expr(operand);
        }

        LogicExpr::Question { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::YesNoQuestion { body } => {
            v.visit_expr(body);
        }

        LogicExpr::Atom(_) => {}

        LogicExpr::Lambda { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::App { function, argument } => {
            v.visit_expr(function);
            v.visit_expr(argument);
        }

        LogicExpr::Intensional { content, .. } => {
            v.visit_expr(content);
        }

        LogicExpr::Event { predicate, .. } => {
            v.visit_expr(predicate);
        }

        LogicExpr::NeoEvent(data) => {
            for (_, term) in data.roles.iter() {
                v.visit_term(term);
            }
        }

        LogicExpr::Exclamative { body, .. } => {
            v.visit_expr(body);
        }
        LogicExpr::Optative { wish } => {
            v.visit_expr(wish);
        }
        LogicExpr::Implicature { assertion, implicature } => {
            v.visit_expr(assertion);
            v.visit_expr(implicature);
        }
        LogicExpr::Imperative { action } => {
            v.visit_expr(action);
        }

        LogicExpr::SpeechAct { content, .. } => {
            v.visit_expr(content);
        }

        LogicExpr::Counterfactual { antecedent, consequent } => {
            v.visit_expr(antecedent);
            v.visit_expr(consequent);
        }

        LogicExpr::Causal { effect, cause } => {
            v.visit_expr(cause);
            v.visit_expr(effect);
        }
        LogicExpr::Concessive { main, concession } => {
            v.visit_expr(main);
            v.visit_expr(concession);
        }

        LogicExpr::Comparative { subject, object, .. } => {
            v.visit_term(subject);
            v.visit_term(object);
        }

        LogicExpr::Superlative { subject, .. } => {
            v.visit_term(subject);
        }

        LogicExpr::Scopal { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::Control { subject, object, infinitive, .. } => {
            v.visit_term(subject);
            if let Some(obj) = object {
                v.visit_term(obj);
            }
            v.visit_expr(infinitive);
        }

        LogicExpr::Presupposition { assertion, presupposition } => {
            v.visit_expr(assertion);
            v.visit_expr(presupposition);
        }

        LogicExpr::Focus { focused, scope, .. } => {
            v.visit_term(focused);
            v.visit_expr(scope);
        }

        LogicExpr::TemporalAnchor { body, .. } => {
            v.visit_expr(body);
        }

        LogicExpr::Distributive { predicate } => {
            v.visit_expr(predicate);
        }

        LogicExpr::GroupQuantifier { restriction, body, .. } => {
            v.visit_expr(restriction);
            v.visit_expr(body);
        }
    }
}

pub fn walk_term<'a, V: Visitor<'a>>(v: &mut V, term: &'a Term<'a>) {
    match term {
        Term::Constant(_) | Term::Variable(_) | Term::Sigma(_) | Term::Intension(_) | Term::Kind(_) | Term::Value { .. } => {}

        Term::Function(_, args) => {
            for arg in *args {
                v.visit_term(arg);
            }
        }

        Term::Group(members) => {
            for m in *members {
                v.visit_term(m);
            }
        }

        Term::Possessed { possessor, .. } => {
            v.visit_term(possessor);
        }

        Term::Proposition(expr) => {
            v.visit_expr(expr);
        }
    }
}

pub fn walk_np<'a, V: Visitor<'a>>(v: &mut V, np: &'a NounPhrase<'a>) {
    if let Some(poss) = np.possessor {
        v.visit_np(poss);
    }
    for pp in np.pps.iter() {
        v.visit_expr(pp);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use logicaffeine_base::Symbol;

    struct VariableCollector {
        variables: Vec<Symbol>,
    }

    impl<'a> Visitor<'a> for VariableCollector {
        fn visit_term(&mut self, term: &'a Term<'a>) {
            if let Term::Variable(sym) = term {
                self.variables.push(*sym);
            }
            walk_term(self, term);
        }
    }

    struct ExprCounter {
        count: usize,
    }

    impl<'a> Visitor<'a> for ExprCounter {
        fn visit_expr(&mut self, expr: &'a LogicExpr<'a>) {
            self.count += 1;
            walk_expr(self, expr);
        }
    }

    #[test]
    fn variable_collector_finds_variables() {
        use logicaffeine_base::Arena;
        use logicaffeine_base::Interner;

        let mut interner = Interner::new();
        let x = interner.intern("x");
        let y = interner.intern("y");

        let term_arena: Arena<Term> = Arena::new();
        let terms = term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]);

        let expr_arena: Arena<LogicExpr> = Arena::new();
        let pred = interner.intern("P");
        let expr = expr_arena.alloc(LogicExpr::Predicate { name: pred, args: terms, world: None });

        let mut collector = VariableCollector { variables: vec![] };
        collector.visit_expr(expr);

        assert_eq!(collector.variables.len(), 2);
        assert!(collector.variables.contains(&x));
        assert!(collector.variables.contains(&y));
    }

    #[test]
    fn expr_counter_counts_nested() {
        use logicaffeine_base::Arena;
        use logicaffeine_base::Interner;
        use crate::token::TokenType;

        let mut interner = Interner::new();
        let p = interner.intern("P");
        let q = interner.intern("Q");

        let expr_arena: Arena<LogicExpr> = Arena::new();

        let left = expr_arena.alloc(LogicExpr::Atom(p));
        let right = expr_arena.alloc(LogicExpr::Atom(q));
        let binary = expr_arena.alloc(LogicExpr::BinaryOp {
            left,
            op: TokenType::And,
            right,
        });

        let mut counter = ExprCounter { count: 0 };
        counter.visit_expr(binary);

        assert_eq!(counter.count, 3);
    }
}