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//!
27//! # Choosing a branch without reading every branch
28//!
29//! `spec/optimizer/36-lowering-and-isel.md` section 36.5 asks for the decision to be on the shape
30//! of the term rather than on the identity of the pattern, which is the difference between a trie
31//! that is a tree and a trie that is a tree with a list at every node. Sharing a prefix already
32//! means the head of a term is tested once rather than once per rule, but it does not say how the
33//! branch is found, and reading a node's branches in the order the rules were written is a scan
34//! over all of them. That costs what the widest node is wide: the x86-64 rule set has a hundred
35//! and sixty seven different heads a pattern can begin with, so choosing the branch for an
36//! `add.i64` meant asking a hundred and sixty six other questions first, once for every
37//! instruction the selector looks at, and a term no rule covers asked all of them.
38//!
39//! So a node holds its branches by the kind of question they ask, and the two kinds that can be
40//! searched are kept sorted: the heads by name and then by how many arguments they take, and the
41//! literals by value. At most one of either can match a subterm, because a term has one head and
42//! a constant has one value, so the order inside those two is not observable and sorting them
43//! costs nothing. Finding the branch is then a binary search, which is eight comparisons at that
44//! widest node rather than a hundred and sixty seven.
45//!
46//! # The order the kinds are tried in
47//!
48//! Which kind is asked first is a heuristic, and section 36.5 is explicit that a heuristic is to
49//! be stated rather than left to be discovered by reading what came out. The order is: the head
50//! of the term, then its value as a literal, then whether it is what an earlier binding took,
51//! then the hole that takes anything. The first three are all concrete and the hole is last,
52//! which is the specificity order above and is the part that decides which rule fires.
53//!
54//! The order among the first three decides nothing in any rule set here, because deciding
55//! something would need one node to ask two kinds of question about one place, and none does: a
56//! literal is only ever written where a pattern has descended into a constant, and a name written
57//! twice is only ever written where the first occurrence put a hole. [`Matcher::shape`] counts
58//! the nodes that mix kinds for exactly this reason, so that a rule set which starts to depend on
59//! the order is a number that changed rather than a surprise in the output.
60
61use std::fmt;
62
63use crate::ast::{Rule, Term, TermKind};
64use crate::error::Error;
65
66/// One step of a flattened pattern.
67#[derive(Debug, Clone, PartialEq, Eq)]
68enum Step {
69    /// The subterm here must be this head applied to this many arguments.
70    App { head: String, arity: usize },
71    /// The subterm here must be this literal.
72    Int(i128),
73    /// Anything goes here, and it is remembered under this name.
74    Bind(String),
75    /// The subterm here must be what this binding of the same pattern already took, which is
76    /// what the second occurrence of a name means.
77    Same(usize),
78}
79
80/// One node of the trie.
81///
82/// The branches are held by the kind of question they ask rather than in one list, which is what
83/// lets the two searchable kinds be searched. A hole is not one of them: it is not a question, it
84/// is what is left when none of the questions was answered.
85#[derive(Debug, Default)]
86pub(crate) struct Node {
87    /// The branches taken on the head of the subterm, sorted by name and then by how many
88    /// arguments it takes.
89    pub(crate) heads: Vec<(String, usize, usize)>,
90    /// The branches taken on the value of a subterm that is a constant, sorted by value.
91    pub(crate) ints: Vec<(i128, usize)>,
92    /// The branches taken when the subterm is what an earlier binding took, in the order the
93    /// rules were written, because two of them can match one subterm.
94    pub(crate) same: Vec<(usize, usize)>,
95    /// The branch that takes anything, and the name it binds it under.
96    pub(crate) wildcard: Option<(String, usize)>,
97    /// The rules that end here, in the order they were written. Every one but the last has a
98    /// guard, and the first whose guard holds is the one that fires.
99    pub(crate) accept: Vec<usize>,
100}
101
102impl Node {
103    /// Every branch this node has, in the order the walk tries them, which is what printing it
104    /// and counting it are both written against.
105    fn branches(&self) -> impl Iterator<Item = (Shown<'_>, usize)> {
106        let heads = self.heads.iter().map(|(head, arity, next)| (Shown::App(head, *arity), *next));
107        let ints = self.ints.iter().map(|&(value, next)| (Shown::Int(value), next));
108        let same = self.same.iter().map(|&(index, next)| (Shown::Same(index), next));
109        heads.chain(ints).chain(same)
110    }
111
112    /// How many kinds of question this node asks. More than one means the order the kinds are
113    /// tried in decides which rule fires here.
114    fn kinds(&self) -> usize {
115        usize::from(!self.heads.is_empty())
116            + usize::from(!self.ints.is_empty())
117            + usize::from(!self.same.is_empty())
118    }
119}
120
121/// One branch of a node as something to print.
122enum Shown<'a> {
123    App(&'a str, usize),
124    Int(i128),
125    Same(usize),
126}
127
128/// What a rule set costs to match against, which is what the header of a generated table says
129/// and what a test that the tree is a tree asserts.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct Shape {
132    /// How many nodes the trie has.
133    pub nodes: usize,
134    /// How many branches the widest node has, which is what a scan over it would cost.
135    pub widest: usize,
136    /// How many comparisons a binary search over that many branches takes.
137    pub search: usize,
138    /// How many nodes ask more than one kind of question, and so depend on the order the kinds
139    /// are tried in. Nothing shipped here does.
140    pub mixed: usize,
141}
142
143/// The automaton a rule set compiles into.
144#[derive(Debug)]
145pub struct Matcher {
146    pub(crate) nodes: Vec<Node>,
147}
148
149/// What a successful match found.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct Match<'t> {
152    /// The index into the rule set of the rule that fired.
153    pub rule: usize,
154    /// What the pattern's variables were bound to, in the order the pattern binds them.
155    pub bindings: Vec<(String, &'t Term)>,
156}
157
158impl<'t> Match<'t> {
159    /// What one name was bound to, or nothing if the pattern never bound it.
160    #[must_use]
161    pub fn get(&self, name: &str) -> Option<&'t Term> {
162        self.bindings.iter().find(|(bound, _)| bound == name).map(|(_, term)| *term)
163    }
164}
165
166impl Matcher {
167    /// Compile a rule set.
168    ///
169    /// # Errors
170    ///
171    /// A rule whose pattern is one an earlier rule without a guard already has can never fire,
172    /// and that is reported rather than silently dropped. It is always a mistake: either the
173    /// second rule was meant to say something else, or one of the two should not be there. After
174    /// a rule with a guard it is the next thing tried when the guard does not hold.
175    pub fn build(path: &str, rules: &[Rule]) -> Result<Matcher, Vec<Error>> {
176        let mut matcher = Matcher { nodes: vec![Node::default()] };
177        let mut errors = Vec::new();
178
179        for (index, rule) in rules.iter().enumerate() {
180            let mut at = 0;
181            for step in flatten(&rule.pattern) {
182                at = matcher.follow(at, step);
183            }
184            let accept = &mut matcher.nodes[at].accept;
185            match accept.iter().find(|&&first| rules[first].guard.is_none()) {
186                Some(&first) => errors.push(Error {
187                    path: path.to_owned(),
188                    line: rule.line,
189                    column: rule.column,
190                    message: format!(
191                        "this rule can never fire, because the rule on line {} matches everything it does",
192                        rules[first].line
193                    ),
194                }),
195                None => accept.push(index),
196            }
197        }
198
199        if !errors.is_empty() {
200            return Err(errors);
201        }
202        matcher.sort();
203        Ok(matcher)
204    }
205
206    /// Add one step at one node, reusing the branch if it is already there.
207    fn follow(&mut self, at: usize, step: Step) -> usize {
208        match step {
209            Step::App { head, arity } => {
210                let found = self.nodes[at]
211                    .heads
212                    .iter()
213                    .find(|(have, count, _)| *have == head && *count == arity);
214                if let Some(&(_, _, next)) = found {
215                    return next;
216                }
217                let next = self.push();
218                self.nodes[at].heads.push((head, arity, next));
219                next
220            }
221            Step::Int(value) => {
222                if let Some(&(_, next)) =
223                    self.nodes[at].ints.iter().find(|(have, _)| *have == value)
224                {
225                    return next;
226                }
227                let next = self.push();
228                self.nodes[at].ints.push((value, next));
229                next
230            }
231            Step::Same(index) => {
232                if let Some(&(_, next)) =
233                    self.nodes[at].same.iter().find(|(have, _)| *have == index)
234                {
235                    return next;
236                }
237                let next = self.push();
238                self.nodes[at].same.push((index, next));
239                next
240            }
241            Step::Bind(name) => {
242                if let Some((_, next)) = &self.nodes[at].wildcard {
243                    // The name is the first one written. Two rules that put different names in
244                    // the same hole are the same automaton, and the binding is reported back
245                    // under the name of the rule that fired rather than under this one.
246                    return *next;
247                }
248                let next = self.push();
249                self.nodes[at].wildcard = Some((name, next));
250                next
251            }
252        }
253    }
254
255    /// Put the searchable branches in the order a search needs them.
256    ///
257    /// This is the last thing the build does, so that everything before it can add a branch by
258    /// pushing. Nothing about which rule fires depends on it: one head matches a term and one
259    /// value matches a constant, so the order inside either list is not something a match can
260    /// observe. What it buys is that the walk can binary search rather than read the list.
261    fn sort(&mut self) {
262        for node in &mut self.nodes {
263            node.heads.sort_by(|(head, arity, _), (other, count, _)| {
264                head.cmp(other).then(arity.cmp(count))
265            });
266            node.ints.sort_by_key(|&(value, _)| value);
267        }
268    }
269
270    fn push(&mut self) -> usize {
271        self.nodes.push(Node::default());
272        self.nodes.len() - 1
273    }
274
275    /// Match one term against the whole rule set, returning the rule that fires.
276    ///
277    /// Guards are not evaluated here, so of rules that share a pattern it is the first.
278    ///
279    /// The term is matched as a whole. Finding the subterms of a function worth matching is the
280    /// selector's job and not this one's.
281    #[must_use]
282    pub fn find<'t>(&self, term: &'t Term) -> Option<Match<'t>> {
283        let mut bindings = Vec::new();
284        let rule = self.run(0, vec![term], &mut bindings)?;
285        Some(Match { rule, bindings })
286    }
287
288    /// Walk the trie and the subject together.
289    ///
290    /// `left` is the subterms still to be matched, innermost last, so that popping gives the
291    /// pre-order the patterns were flattened in.
292    fn run<'t>(
293        &self,
294        at: usize,
295        mut left: Vec<&'t Term>,
296        bindings: &mut Vec<(String, &'t Term)>,
297    ) -> Option<usize> {
298        let Some(subject) = left.pop() else {
299            return self.nodes[at].accept.first().copied();
300        };
301        let node = &self.nodes[at];
302
303        // The head, the value and the repeat, in that order, which is the heuristic the module
304        // doc states. At most one head and at most one value can match, so each of those is a
305        // search rather than a walk, which is the same shape the compiler's own walk has.
306        let mut taken: Vec<usize> = Vec::new();
307        if let TermKind::App { head, args } = &subject.kind {
308            let found = node
309                .heads
310                .binary_search_by(|(have, count, _)| {
311                    have.as_str().cmp(head.as_str()).then(count.cmp(&args.len()))
312                })
313                .ok();
314            taken.extend(found.map(|at| node.heads[at].2));
315        }
316        if let TermKind::Int(value) = &subject.kind {
317            let found = node.ints.binary_search_by(|(have, _)| have.cmp(value)).ok();
318            taken.extend(found.map(|at| node.ints[at].1));
319        }
320        // Written out rather than compared with `==`, because a term carries where it was
321        // written and two occurrences of one name are in two different places.
322        for &(index, next) in &node.same {
323            if bindings.get(index).is_some_and(|(_, bound)| alike(bound, subject)) {
324                taken.push(next);
325            }
326        }
327
328        for next in taken {
329            let mut deeper = left.clone();
330            if let TermKind::App { args, .. } = &subject.kind {
331                deeper.extend(args.iter().rev());
332            }
333            let depth = bindings.len();
334            if let Some(rule) = self.run(next, deeper, bindings) {
335                return Some(rule);
336            }
337            bindings.truncate(depth);
338        }
339
340        // The wildcard is last, which is the whole of what "specificity order" means here.
341        let (name, next) = node.wildcard.as_ref()?;
342        let depth = bindings.len();
343        bindings.push((name.clone(), subject));
344        if let Some(rule) = self.run(*next, left, bindings) {
345            return Some(rule);
346        }
347        bindings.truncate(depth);
348        None
349    }
350
351    /// How many nodes the trie has, which is what a rule set costs to match against.
352    #[must_use]
353    pub fn len(&self) -> usize {
354        self.nodes.len()
355    }
356
357    /// Whether the rule set was empty.
358    #[must_use]
359    pub fn is_empty(&self) -> bool {
360        self.nodes.len() <= 1
361    }
362
363    /// What this rule set costs to match against.
364    ///
365    /// The widest node is the measurement that matters, because it is the one the shape of the
366    /// tree was changed for: it is what a scan would read to the end of and what a search reads
367    /// eight of. It goes in the header of the generated table, where somebody reviewing a rule
368    /// they added can see what adding it did.
369    #[must_use]
370    pub fn shape(&self) -> Shape {
371        let widest = self.nodes.iter().map(|node| node.branches().count()).max().unwrap_or(0);
372        Shape {
373            nodes: self.nodes.len(),
374            widest,
375            search: usize::try_from(widest.next_power_of_two().trailing_zeros()).unwrap_or(0),
376            mixed: self.nodes.iter().filter(|node| node.kinds() > 1).count(),
377        }
378    }
379}
380
381/// Whether two terms say the same thing, ignoring where each of them was written.
382///
383/// A [`Term`] holds its line and column, so the derived equality is equality of two occurrences
384/// and not of two terms. What a repeated name asks is about the terms.
385fn alike(left: &Term, right: &Term) -> bool {
386    match (&left.kind, &right.kind) {
387        (TermKind::Var(a), TermKind::Var(b)) => a == b,
388        (TermKind::Int(a), TermKind::Int(b)) => a == b,
389        (TermKind::App { head: a, args: xs }, TermKind::App { head: b, args: ys }) => {
390            a == b && xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| alike(x, y))
391        }
392        _ => false,
393    }
394}
395
396/// Flatten a pattern into the steps that match it, in the pre-order the matcher walks.
397fn flatten(pattern: &Term) -> Vec<Step> {
398    let mut out = Vec::new();
399    let mut bound: Vec<&str> = Vec::new();
400    push_steps(pattern, &mut bound, &mut out);
401    out
402}
403
404/// `bound` is the names this pattern has bound so far, in order, so that a name written again
405/// becomes a test against the position the first occurrence took. The position is well defined
406/// across rules that share a prefix: sharing a prefix means having consumed the same shape of
407/// subject, so the same number of bindings have been made at any node of the trie.
408fn push_steps<'t>(term: &'t Term, bound: &mut Vec<&'t str>, out: &mut Vec<Step>) {
409    match &term.kind {
410        TermKind::Var(name) => match bound.iter().position(|have| *have == name.as_str()) {
411            Some(index) => out.push(Step::Same(index)),
412            None => {
413                bound.push(name.as_str());
414                out.push(Step::Bind(name.clone()));
415            }
416        },
417        TermKind::Int(value) => out.push(Step::Int(*value)),
418        TermKind::App { head, args } => {
419            out.push(Step::App { head: head.clone(), arity: args.len() });
420            for arg in args {
421                push_steps(arg, bound, out);
422            }
423        }
424    }
425}
426
427impl fmt::Display for Matcher {
428    /// Prints the trie, one branch to a line, indented by depth. This is what makes a rule set's
429    /// shape reviewable: two rules that share a prefix share a line, and a rule that can only be
430    /// reached through a wildcard is visibly the last thing tried.
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        self.show(f, 0, 0)
433    }
434}
435
436impl Matcher {
437    fn show(&self, f: &mut fmt::Formatter<'_>, at: usize, depth: usize) -> fmt::Result {
438        let pad = "  ".repeat(depth);
439        let node = &self.nodes[at];
440        for rule in &node.accept {
441            writeln!(f, "{pad}=> rule {rule}")?;
442        }
443        for (branch, next) in node.branches() {
444            match branch {
445                Shown::App(head, arity) => writeln!(f, "{pad}{head}/{arity}")?,
446                Shown::Int(value) => writeln!(f, "{pad}{value}")?,
447                Shown::Same(index) => writeln!(f, "{pad}same as binding {index}")?,
448            }
449            self.show(f, next, depth + 1)?;
450        }
451        if let Some((name, next)) = &node.wildcard {
452            writeln!(f, "{pad}bind {name}")?;
453            self.show(f, *next, depth + 1)?;
454        }
455        Ok(())
456    }
457}