Skip to main content

rucc_rules/
matcher.rs

1//! Rules to the automaton that matches them.
2//!
3//! A pattern is a tree and the subject is a tree, and the obvious way to match one against the
4//! other is a chain of conditionals per rule. That is what `spec/10-backend.md` says not to
5//! build: with several hundred rules per target it re-tests the same opcode hundreds of times,
6//! and it puts the order the rules are tried in beyond anybody's control.
7//!
8//! What is built instead is a trie over the patterns, flattened. Every pattern becomes a
9//! sequence of steps read in pre-order, and patterns that begin the same way share the steps
10//! they agree on, so testing that a term is an `add.i64` happens once no matter how many rules
11//! begin with one. Matching walks the subject in the same pre-order, which is what makes the
12//! sequence well defined: at any node of the trie, every rule that reaches it has consumed the
13//! same shape of subject, so there is one stack of remaining subterms rather than one per rule.
14//!
15//! Specificity falls out of the shape rather than being sorted for. At each node the concrete
16//! tests are tried before the wildcard, so a rule that names an operand is always tried before
17//! a rule that takes anything there, which is the maximal munch that document asks for. Among
18//! rules that are equally specific the first one written wins, which is what `-O0` wants and is
19//! what the single-pass mode in section 10.3 is defined to do.
20//!
21//! A name written twice in one pattern is a claim that the two places hold the same thing, which
22//! is how the identities of `spec/optimizer/13-rewrite-rules.md` section 13.4 say `x & x` and
23//! `x - x`. The second occurrence becomes a test rather than a binding, so it costs one
24//! comparison and sits with the other concrete tests, ahead of the wildcard, where a rule about
25//! one value in both operands belongs.
26
27use std::fmt;
28
29use crate::ast::{Rule, Term, TermKind};
30use crate::error::Error;
31
32/// One step of a flattened pattern.
33#[derive(Debug, Clone, PartialEq, Eq)]
34enum Step {
35    /// The subterm here must be this head applied to this many arguments.
36    App { head: String, arity: usize },
37    /// The subterm here must be this literal.
38    Int(i128),
39    /// Anything goes here, and it is remembered under this name.
40    Bind(String),
41    /// The subterm here must be what this binding of the same pattern already took, which is
42    /// what the second occurrence of a name means.
43    Same(usize),
44}
45
46/// A test on one subterm. This is [`Step`] without the wildcard, because a wildcard is not a
47/// test: it is the branch taken when no test matched.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub(crate) enum Test {
50    App { head: String, arity: usize },
51    Int(i128),
52    Same(usize),
53}
54
55/// One node of the trie.
56#[derive(Debug, Default)]
57pub(crate) struct Node {
58    /// The concrete tests, in the order they were first written, tried before the wildcard.
59    pub(crate) tests: Vec<(Test, usize)>,
60    /// The branch that takes anything, and the name it binds it under.
61    pub(crate) wildcard: Option<(String, usize)>,
62    /// The rule that ends here, if one does.
63    pub(crate) accept: Option<usize>,
64}
65
66/// The automaton a rule set compiles into.
67#[derive(Debug)]
68pub struct Matcher {
69    pub(crate) nodes: Vec<Node>,
70}
71
72/// What a successful match found.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Match<'t> {
75    /// The index into the rule set of the rule that fired.
76    pub rule: usize,
77    /// What the pattern's variables were bound to, in the order the pattern binds them.
78    pub bindings: Vec<(String, &'t Term)>,
79}
80
81impl<'t> Match<'t> {
82    /// What one name was bound to, or nothing if the pattern never bound it.
83    #[must_use]
84    pub fn get(&self, name: &str) -> Option<&'t Term> {
85        self.bindings.iter().find(|(bound, _)| bound == name).map(|(_, term)| *term)
86    }
87}
88
89impl Matcher {
90    /// Compile a rule set.
91    ///
92    /// # Errors
93    ///
94    /// A rule whose pattern is one an earlier rule already has can never fire, and that is
95    /// reported rather than silently dropped. It is always a mistake: either the second rule was
96    /// meant to say something else, or one of the two should not be there.
97    pub fn build(path: &str, rules: &[Rule]) -> Result<Matcher, Vec<Error>> {
98        let mut matcher = Matcher { nodes: vec![Node::default()] };
99        let mut errors = Vec::new();
100
101        for (index, rule) in rules.iter().enumerate() {
102            let mut at = 0;
103            for step in flatten(&rule.pattern) {
104                at = matcher.follow(at, step);
105            }
106            match matcher.nodes[at].accept {
107                Some(first) => errors.push(Error {
108                    path: path.to_owned(),
109                    line: rule.line,
110                    column: rule.column,
111                    message: format!(
112                        "this rule can never fire, because the rule on line {} matches everything it does",
113                        rules[first].line
114                    ),
115                }),
116                None => matcher.nodes[at].accept = Some(index),
117            }
118        }
119
120        if errors.is_empty() { Ok(matcher) } else { Err(errors) }
121    }
122
123    /// Add one step at one node, reusing the branch if it is already there.
124    fn follow(&mut self, at: usize, step: Step) -> usize {
125        let test = match step {
126            Step::App { head, arity } => Test::App { head, arity },
127            Step::Int(value) => Test::Int(value),
128            Step::Same(index) => Test::Same(index),
129            Step::Bind(name) => {
130                if let Some((_, next)) = &self.nodes[at].wildcard {
131                    // The name is the first one written. Two rules that put different names in
132                    // the same hole are the same automaton, and the binding is reported back
133                    // under the name of the rule that fired rather than under this one.
134                    return *next;
135                }
136                let next = self.push();
137                self.nodes[at].wildcard = Some((name, next));
138                return next;
139            }
140        };
141        if let Some((_, next)) = self.nodes[at].tests.iter().find(|(have, _)| *have == test) {
142            return *next;
143        }
144        let next = self.push();
145        self.nodes[at].tests.push((test, next));
146        next
147    }
148
149    fn push(&mut self) -> usize {
150        self.nodes.push(Node::default());
151        self.nodes.len() - 1
152    }
153
154    /// Match one term against the whole rule set, returning the rule that fires.
155    ///
156    /// The term is matched as a whole. Finding the subterms of a function worth matching is the
157    /// selector's job and not this one's.
158    #[must_use]
159    pub fn find<'t>(&self, term: &'t Term) -> Option<Match<'t>> {
160        let mut bindings = Vec::new();
161        let rule = self.run(0, vec![term], &mut bindings)?;
162        Some(Match { rule, bindings })
163    }
164
165    /// Walk the trie and the subject together.
166    ///
167    /// `left` is the subterms still to be matched, innermost last, so that popping gives the
168    /// pre-order the patterns were flattened in.
169    fn run<'t>(
170        &self,
171        at: usize,
172        mut left: Vec<&'t Term>,
173        bindings: &mut Vec<(String, &'t Term)>,
174    ) -> Option<usize> {
175        let Some(subject) = left.pop() else {
176            return self.nodes[at].accept;
177        };
178        let node = &self.nodes[at];
179
180        for (test, next) in &node.tests {
181            let matched = match (test, &subject.kind) {
182                (Test::Int(want), TermKind::Int(have)) => want == have,
183                (Test::App { head, arity }, TermKind::App { head: name, args }) => {
184                    head == name && *arity == args.len()
185                }
186                // Written out rather than compared with `==`, because a term carries where it
187                // was written and two occurrences of one name are in two different places.
188                (Test::Same(index), _) => {
189                    bindings.get(*index).is_some_and(|(_, bound)| alike(bound, subject))
190                }
191                _ => false,
192            };
193            if !matched {
194                continue;
195            }
196            let mut deeper = left.clone();
197            if let TermKind::App { args, .. } = &subject.kind {
198                deeper.extend(args.iter().rev());
199            }
200            let depth = bindings.len();
201            if let Some(rule) = self.run(*next, deeper, bindings) {
202                return Some(rule);
203            }
204            bindings.truncate(depth);
205        }
206
207        // The wildcard is last, which is the whole of what "specificity order" means here.
208        let (name, next) = node.wildcard.as_ref()?;
209        let depth = bindings.len();
210        bindings.push((name.clone(), subject));
211        if let Some(rule) = self.run(*next, left, bindings) {
212            return Some(rule);
213        }
214        bindings.truncate(depth);
215        None
216    }
217
218    /// How many nodes the trie has, which is what a rule set costs to match against.
219    #[must_use]
220    pub fn len(&self) -> usize {
221        self.nodes.len()
222    }
223
224    /// Whether the rule set was empty.
225    #[must_use]
226    pub fn is_empty(&self) -> bool {
227        self.nodes.len() <= 1
228    }
229}
230
231/// Whether two terms say the same thing, ignoring where each of them was written.
232///
233/// A [`Term`] holds its line and column, so the derived equality is equality of two occurrences
234/// and not of two terms. What a repeated name asks is about the terms.
235fn alike(left: &Term, right: &Term) -> bool {
236    match (&left.kind, &right.kind) {
237        (TermKind::Var(a), TermKind::Var(b)) => a == b,
238        (TermKind::Int(a), TermKind::Int(b)) => a == b,
239        (TermKind::App { head: a, args: xs }, TermKind::App { head: b, args: ys }) => {
240            a == b && xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| alike(x, y))
241        }
242        _ => false,
243    }
244}
245
246/// Flatten a pattern into the steps that match it, in the pre-order the matcher walks.
247fn flatten(pattern: &Term) -> Vec<Step> {
248    let mut out = Vec::new();
249    let mut bound: Vec<&str> = Vec::new();
250    push_steps(pattern, &mut bound, &mut out);
251    out
252}
253
254/// `bound` is the names this pattern has bound so far, in order, so that a name written again
255/// becomes a test against the position the first occurrence took. The position is well defined
256/// across rules that share a prefix: sharing a prefix means having consumed the same shape of
257/// subject, so the same number of bindings have been made at any node of the trie.
258fn push_steps<'t>(term: &'t Term, bound: &mut Vec<&'t str>, out: &mut Vec<Step>) {
259    match &term.kind {
260        TermKind::Var(name) => match bound.iter().position(|have| *have == name.as_str()) {
261            Some(index) => out.push(Step::Same(index)),
262            None => {
263                bound.push(name.as_str());
264                out.push(Step::Bind(name.clone()));
265            }
266        },
267        TermKind::Int(value) => out.push(Step::Int(*value)),
268        TermKind::App { head, args } => {
269            out.push(Step::App { head: head.clone(), arity: args.len() });
270            for arg in args {
271                push_steps(arg, bound, out);
272            }
273        }
274    }
275}
276
277impl fmt::Display for Matcher {
278    /// Prints the trie, one branch to a line, indented by depth. This is what makes a rule set's
279    /// shape reviewable: two rules that share a prefix share a line, and a rule that can only be
280    /// reached through a wildcard is visibly the last thing tried.
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        self.show(f, 0, 0)
283    }
284}
285
286impl Matcher {
287    fn show(&self, f: &mut fmt::Formatter<'_>, at: usize, depth: usize) -> fmt::Result {
288        let pad = "  ".repeat(depth);
289        let node = &self.nodes[at];
290        if let Some(rule) = node.accept {
291            writeln!(f, "{pad}=> rule {rule}")?;
292        }
293        for (test, next) in &node.tests {
294            match test {
295                Test::App { head, arity } => writeln!(f, "{pad}{head}/{arity}")?,
296                Test::Int(value) => writeln!(f, "{pad}{value}")?,
297                Test::Same(index) => writeln!(f, "{pad}same as binding {index}")?,
298            }
299            self.show(f, *next, depth + 1)?;
300        }
301        if let Some((name, next)) = &node.wildcard {
302            writeln!(f, "{pad}bind {name}")?;
303            self.show(f, *next, depth + 1)?;
304        }
305        Ok(())
306    }
307}