Skip to main content

ocas_rewrite/
matcher.rs

1//! Pattern matching engine for oCAS.
2//!
3//! The matcher binds [`Pattern`] wildcards to [`Atom`]
4//! sub-expressions. It supports associative/commutative matching for `Add`
5//! and `Mul` using backtracking, and sequence wildcards for ordered argument
6//! lists such as function arguments.
7
8use std::collections::HashMap;
9
10use ocas_atom::{Atom, AtomNode, Symbol};
11
12use crate::pattern::{Pattern, WildcardLevel};
13
14/// A collection of wildcard bindings produced by a successful match.
15///
16/// # Example
17///
18/// ```
19/// use ocas_atom::AtomArena;
20/// use ocas_atom::Symbol;
21/// use ocas_core::arena::Arena;
22/// use ocas_rewrite::matcher::{match_pattern, Bindings, MatchValue};
23/// use ocas_rewrite::pattern::{Pattern, WildcardLevel};
24///
25/// let arena = Arena::new();
26/// let ctx = AtomArena::new(&arena);
27/// let x = ctx.var("x");
28/// let pat = Pattern::Wildcard(Symbol::new("w"), WildcardLevel::Single);
29/// let bindings = match_pattern(pat, x).unwrap();
30/// let value = bindings.get(Symbol::new("w")).unwrap();
31/// assert!(matches!(value, MatchValue::Single(v) if v.to_string() == "x"));
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct Bindings<'a> {
35    map: HashMap<Symbol, MatchValue<'a>>,
36}
37
38impl<'a> Bindings<'a> {
39    /// Create an empty binding set.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Look up a binding by wildcard name.
45    pub fn get(&self, name: Symbol) -> Option<&MatchValue<'a>> {
46        self.map.get(&name)
47    }
48
49    /// Insert a single-atom binding. Returns `Err` if the name is already bound
50    /// to a different value.
51    fn insert_single(&mut self, name: Symbol, value: Atom<'a>) -> Result<(), MatchError> {
52        match self.map.get(&name) {
53            Some(MatchValue::Single(existing)) if *existing == value => Ok(()),
54            Some(_) => Err(MatchError::InconsistentBinding),
55            None => {
56                self.map.insert(name, MatchValue::Single(value));
57                Ok(())
58            }
59        }
60    }
61
62    /// Insert a multi-atom binding (used for sequence/blank null sequence).
63    fn insert_sequence(&mut self, name: Symbol, value: &'a [Atom<'a>]) -> Result<(), MatchError> {
64        match self.map.get(&name) {
65            Some(MatchValue::Sequence(existing)) if *existing == value => Ok(()),
66            Some(_) => Err(MatchError::InconsistentBinding),
67            None => {
68                self.map.insert(name, MatchValue::Sequence(value));
69                Ok(())
70            }
71        }
72    }
73}
74
75/// A value bound to a wildcard.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum MatchValue<'a> {
78    /// A single atom binding.
79    Single(Atom<'a>),
80    /// A slice of atoms bound to a sequence wildcard.
81    Sequence(&'a [Atom<'a>]),
82}
83
84/// Errors that can occur during matching.
85///
86/// # Example
87///
88/// ```
89/// use ocas_atom::AtomArena;
90/// use ocas_atom::Symbol;
91/// use ocas_core::arena::Arena;
92/// use ocas_rewrite::matcher::{match_pattern, MatchError};
93/// use ocas_rewrite::pattern::{Pattern, WildcardLevel};
94///
95/// let arena = Arena::new();
96/// let ctx = AtomArena::new(&arena);
97/// let x = ctx.var("x");
98/// let y = ctx.var("y");
99/// let pat = Pattern::Literal(x);
100/// let err = match_pattern(pat, y).unwrap_err();
101/// assert!(matches!(err, MatchError::NoMatch));
102/// ```
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum MatchError {
105    /// The pattern did not match the atom.
106    NoMatch,
107    /// A wildcard name was bound to two different values.
108    InconsistentBinding,
109}
110
111impl std::fmt::Display for MatchError {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            MatchError::NoMatch => write!(f, "pattern did not match"),
115            MatchError::InconsistentBinding => write!(f, "inconsistent wildcard binding"),
116        }
117    }
118}
119
120impl std::error::Error for MatchError {}
121
122/// Match a pattern against an atom, returning bindings on success.
123///
124/// `Add` and `Mul` are matched associatively and commutatively: arguments are
125/// sorted and literal patterns are matched before single wildcards. Sequence
126/// wildcards are not supported inside `Add`/`Mul` in this simplified matcher;
127/// they are supported for ordered argument lists such as function arguments.
128///
129/// # Example
130///
131/// ```
132/// use ocas_atom::AtomArena;
133/// use ocas_atom::Symbol;
134/// use ocas_core::arena::Arena;
135/// use ocas_rewrite::matcher::{match_pattern, MatchValue};
136/// use ocas_rewrite::pattern::{Pattern, WildcardLevel};
137///
138/// let arena = Arena::new();
139/// let ctx = AtomArena::new(&arena);
140/// let x = ctx.var("x");
141/// let y = ctx.var("y");
142/// let sum = ctx.add(&[x, y]);
143/// let pat = Pattern::Add(vec![
144///     Pattern::Wildcard(Symbol::new("a"), WildcardLevel::Single),
145///     Pattern::Wildcard(Symbol::new("b"), WildcardLevel::Single),
146/// ]);
147/// let bindings = match_pattern(pat, sum).unwrap();
148/// let a = bindings.get(Symbol::new("a")).unwrap();
149/// let b = bindings.get(Symbol::new("b")).unwrap();
150/// assert!(matches!(a, MatchValue::Single(v) if v.to_string() == "x"));
151/// assert!(matches!(b, MatchValue::Single(v) if v.to_string() == "y"));
152/// ```
153pub fn match_pattern<'a>(pattern: Pattern<'a>, atom: Atom<'a>) -> Result<Bindings<'a>, MatchError> {
154    let mut bindings = Bindings::new();
155    match_atom(&mut bindings, pattern, atom)?;
156    Ok(bindings)
157}
158
159fn match_atom<'a>(
160    bindings: &mut Bindings<'a>,
161    pattern: Pattern<'a>,
162    atom: Atom<'a>,
163) -> Result<(), MatchError> {
164    match pattern {
165        Pattern::Literal(p) => {
166            if p == atom {
167                Ok(())
168            } else {
169                Err(MatchError::NoMatch)
170            }
171        }
172        Pattern::Wildcard(name, WildcardLevel::Single) => bindings.insert_single(name, atom),
173        Pattern::Wildcard(name, WildcardLevel::Sequence) => match atom.node() {
174            AtomNode::Add(args) | AtomNode::Mul(args) | AtomNode::Fun(_, args) => {
175                if args.is_empty() {
176                    Err(MatchError::NoMatch)
177                } else {
178                    bindings.insert_sequence(name, args)
179                }
180            }
181            _ => Err(MatchError::NoMatch),
182        },
183        Pattern::Wildcard(name, WildcardLevel::NullSequence) => match atom.node() {
184            AtomNode::Add(args) | AtomNode::Mul(args) | AtomNode::Fun(_, args) => {
185                bindings.insert_sequence(name, args)
186            }
187            _ => Err(MatchError::NoMatch),
188        },
189        Pattern::Add(pats) => match atom.node() {
190            AtomNode::Add(args) => match_nary(bindings, &pats, args, true),
191            _ => Err(MatchError::NoMatch),
192        },
193        Pattern::Mul(pats) => match atom.node() {
194            AtomNode::Mul(args) => match_nary(bindings, &pats, args, true),
195            _ => Err(MatchError::NoMatch),
196        },
197        Pattern::Pow(p_box) => match atom.node() {
198            AtomNode::Pow(base, exp) => {
199                let (p_base, p_exp) = *p_box;
200                match_atom(bindings, p_base, *base)?;
201                match_atom(bindings, p_exp, *exp)
202            }
203            _ => Err(MatchError::NoMatch),
204        },
205        Pattern::Fun(p_name, p_args) => match atom.node() {
206            AtomNode::Fun(name, args) if *name == p_name => {
207                match_nary(bindings, &p_args, args, false)
208            }
209            _ => Err(MatchError::NoMatch),
210        },
211    }
212}
213
214/// Match a pattern list against an argument list. For `Add` and `Mul` we sort
215/// the target atoms and match literal/sequence patterns greedily; for `Fun`
216/// arguments we keep the order and only allow a single trailing null-sequence
217/// wildcard to absorb leftovers.
218fn match_nary<'a>(
219    bindings: &mut Bindings<'a>,
220    patterns: &'_ [Pattern<'a>],
221    atoms: &'a [Atom<'a>],
222    associative_commutative: bool,
223) -> Result<(), MatchError> {
224    if associative_commutative {
225        match_nary_ac(bindings, patterns, atoms)
226    } else {
227        match_nary_ordered(bindings, patterns, atoms)
228    }
229}
230
231/// Ordered matching for function arguments. Supports at most one trailing null
232/// sequence wildcard. Sequence wildcards are not supported here.
233fn match_nary_ordered<'a>(
234    bindings: &mut Bindings<'a>,
235    patterns: &'_ [Pattern<'a>],
236    atoms: &'a [Atom<'a>],
237) -> Result<(), MatchError> {
238    let mut pat_idx = 0;
239    let mut atom_idx = 0;
240    while pat_idx < patterns.len() {
241        let pat = &patterns[pat_idx];
242        match pat {
243            Pattern::Wildcard(name, WildcardLevel::NullSequence)
244                if pat_idx == patterns.len() - 1 =>
245            {
246                let rest = &atoms[atom_idx..];
247                bindings.insert_sequence(*name, rest)?;
248                atom_idx = atoms.len();
249                pat_idx += 1;
250            }
251            Pattern::Wildcard(name, WildcardLevel::Single) => {
252                if atom_idx >= atoms.len() {
253                    return Err(MatchError::NoMatch);
254                }
255                bindings.insert_single(*name, atoms[atom_idx])?;
256                atom_idx += 1;
257                pat_idx += 1;
258            }
259            _ => {
260                if atom_idx >= atoms.len() {
261                    return Err(MatchError::NoMatch);
262                }
263                match_atom(bindings, pat.clone(), atoms[atom_idx])?;
264                atom_idx += 1;
265                pat_idx += 1;
266            }
267        }
268    }
269    if atom_idx == atoms.len() {
270        Ok(())
271    } else {
272        Err(MatchError::NoMatch)
273    }
274}
275
276/// Associative/commutative matching for `Add` and `Mul`. We sort the target
277/// atoms by their natural ordering and match literals first, then single
278/// wildcards. Sequence wildcards are not supported in this simplified matcher
279/// because the sorted permutation of `atoms` cannot be borrowed as a single
280/// contiguous slice.
281fn match_nary_ac<'a>(
282    bindings: &mut Bindings<'a>,
283    patterns: &'_ [Pattern<'a>],
284    atoms: &'a [Atom<'a>],
285) -> Result<(), MatchError> {
286    if patterns.is_empty() {
287        return if atoms.is_empty() {
288            Ok(())
289        } else {
290            Err(MatchError::NoMatch)
291        };
292    }
293
294    if patterns.iter().any(|p| {
295        matches!(
296            p,
297            Pattern::Wildcard(_, WildcardLevel::Sequence | WildcardLevel::NullSequence)
298        )
299    }) {
300        return Err(MatchError::NoMatch);
301    }
302
303    let mut sorted_atoms: Vec<Atom<'a>> = atoms.to_vec();
304    sorted_atoms.sort();
305
306    let mut matched = vec![false; sorted_atoms.len()];
307    let mut single_wildcards: Vec<usize> = Vec::new();
308    let mut literal_indices: Vec<usize> = Vec::new();
309
310    for (i, pat) in patterns.iter().enumerate() {
311        match pat {
312            Pattern::Wildcard(_, WildcardLevel::Single) => single_wildcards.push(i),
313            _ => literal_indices.push(i),
314        }
315    }
316
317    // Match literals first.
318    for pat_idx in literal_indices {
319        let pat = &patterns[pat_idx];
320        let mut found = false;
321        for (i, atom) in sorted_atoms.iter().enumerate() {
322            if matched[i] {
323                continue;
324            }
325            let mut probe = bindings.clone();
326            if match_atom(&mut probe, pat.clone(), *atom).is_ok() {
327                *bindings = probe;
328                matched[i] = true;
329                found = true;
330                break;
331            }
332        }
333        if !found {
334            return Err(MatchError::NoMatch);
335        }
336    }
337
338    // Match single wildcards against the remaining atoms.
339    let remaining: Vec<Atom<'a>> = sorted_atoms
340        .iter()
341        .enumerate()
342        .filter(|(i, _)| !matched[*i])
343        .map(|(_, a)| *a)
344        .collect();
345    if remaining.len() != single_wildcards.len() {
346        return Err(MatchError::NoMatch);
347    }
348    for (pat_idx, atom) in single_wildcards.iter().zip(remaining.iter()) {
349        if let Pattern::Wildcard(name, WildcardLevel::Single) = &patterns[*pat_idx] {
350            bindings.insert_single(*name, *atom)?;
351        }
352    }
353
354    Ok(())
355}
356
357impl<'a> Pattern<'a> {
358    fn _wildcard_level(&self) -> Option<WildcardLevel> {
359        match self {
360            Pattern::Wildcard(_, level) => Some(*level),
361            _ => None,
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use ocas_atom::AtomArena;
370    use ocas_core::arena::Arena;
371
372    struct VecAlloc;
373
374    impl<'a> crate::pattern::PatternAlloc<'a> for VecAlloc {
375        fn alloc_slice(&self, items: &[Pattern<'a>]) -> &'a [Pattern<'a>] {
376            let leaked: Box<[Pattern<'a>]> = items.to_vec().into_boxed_slice();
377            Box::leak(leaked)
378        }
379    }
380
381    fn pat_atom<'a>(ctx: &'a AtomArena<'a>, alloc: &'a VecAlloc, s: &'a str) -> Pattern<'a> {
382        let atom = ocas_parse::parse(ctx, s).unwrap();
383        Pattern::from_atom(alloc, atom)
384    }
385
386    #[test]
387    fn single_wildcard_binds_any_atom() {
388        let arena = Arena::new();
389        let ctx = AtomArena::new(&arena);
390        let alloc = VecAlloc;
391        let pat = pat_atom(&ctx, &alloc, "x_");
392        let y = ctx.var("y");
393        let bindings = match_pattern(pat, y).unwrap();
394        assert!(matches!(bindings.get(Symbol::new("x")), Some(MatchValue::Single(a)) if *a == y));
395    }
396
397    #[test]
398    fn add_pattern_matches_two_atoms() {
399        let arena = Arena::new();
400        let ctx = AtomArena::new(&arena);
401        let alloc = VecAlloc;
402        let pat = pat_atom(&ctx, &alloc, "x_ + y_");
403        let x = ctx.var("x");
404        let y = ctx.var("y");
405        let sum = ctx.add(&[x, y]);
406        let bindings = match_pattern(pat, sum).unwrap();
407        assert!(matches!(
408            bindings.get(Symbol::new("x")),
409            Some(MatchValue::Single(a)) if *a == x
410        ));
411        assert!(matches!(
412            bindings.get(Symbol::new("y")),
413            Some(MatchValue::Single(a)) if *a == y
414        ));
415    }
416
417    #[test]
418    fn add_pattern_ignores_order() {
419        let arena = Arena::new();
420        let ctx = AtomArena::new(&arena);
421        let alloc = VecAlloc;
422        let pat = pat_atom(&ctx, &alloc, "x_ + y_");
423        let a = ctx.var("a");
424        let b = ctx.var("b");
425        let sum = ctx.add(&[b, a]);
426        let bindings = match_pattern(pat, sum).unwrap();
427        assert!(matches!(
428            bindings.get(Symbol::new("x")),
429            Some(MatchValue::Single(atom)) if *atom == a
430        ));
431        assert!(matches!(
432            bindings.get(Symbol::new("y")),
433            Some(MatchValue::Single(atom)) if *atom == b
434        ));
435    }
436
437    #[test]
438    fn add_pattern_requires_exact_match_count() {
439        let arena = Arena::new();
440        let ctx = AtomArena::new(&arena);
441        let alloc = VecAlloc;
442        let pat = pat_atom(&ctx, &alloc, "x_ + y_");
443        let a = ctx.var("a");
444        let b = ctx.var("b");
445        let c = ctx.var("c");
446        let sum = ctx.add(&[a, b, c]);
447        assert!(match_pattern(pat, sum).is_err());
448    }
449
450    #[test]
451    fn inconsistent_binding_fails() {
452        let arena = Arena::new();
453        let ctx = AtomArena::new(&arena);
454        let alloc = VecAlloc;
455        let pat = pat_atom(&ctx, &alloc, "x_ + x_");
456        let a = ctx.var("a");
457        let b = ctx.var("b");
458        let sum = ctx.add(&[a, b]);
459        assert!(match_pattern(pat, sum).is_err());
460    }
461
462    #[test]
463    fn sequence_wildcard_in_fun_absorbs_remainder() {
464        let arena = Arena::new();
465        let ctx = AtomArena::new(&arena);
466        let alloc = VecAlloc;
467        let pat = pat_atom(&ctx, &alloc, "f(x_, rest___)");
468        let a = ctx.var("a");
469        let b = ctx.var("b");
470        let c = ctx.var("c");
471        let f = ctx.fun("f", &[a, b, c]);
472        let bindings = match_pattern(pat, f).unwrap();
473        assert!(matches!(
474            bindings.get(Symbol::new("x")),
475            Some(MatchValue::Single(atom)) if *atom == a
476        ));
477        let rest = bindings.get(Symbol::new("rest")).unwrap();
478        assert!(matches!(rest, MatchValue::Sequence(s) if s.len() == 2));
479    }
480}