Skip to main content

rucc_base/
rules.rs

1//! Matching a set of rules against a term.
2//!
3//! Design: `spec/10-backend.md` section 10.2 and `spec/optimizer/13-rewrite-rules.md`. The rules
4//! themselves are rule files, one per rule set, and the automaton they compile into is generated
5//! by `rucc-rules` when the crate that owns the file is built. What is here is the walk over
6//! that automaton, which is the same walk for every rule set and is written once.
7//!
8//! # Why this is at the bottom of the stack
9//!
10//! Two crates match with a generated table and neither can see the other. `rucc-codegen` lowers
11//! IR to machine terms and `rucc-opt` rewrites IR to IR, and a lowering and a rewrite are the
12//! same claim about two terms, so they are the same trie and the same walk. Putting the walk
13//! here rather than in either of them is what keeps that true rather than merely intended, and
14//! it costs nothing: none of this knows what an instruction is, what a value is, or what C is.
15//!
16//! # What a subject is
17//!
18//! A rule matches a term, and the compiler does not have terms: it has a function full of
19//! instructions, and what a pattern is about is one of them and whatever it was computed from.
20//! So the walk is written against [`Subject`], which is the three questions the automaton asks
21//! of whatever it is matching, and a caller answers them out of the IR without building a term
22//! to be thrown away. A test can answer them out of anything at all, which is what the tests at
23//! the bottom of this file do.
24//!
25//! # What a match gives back
26//!
27//! The rule that fired and what its pattern bound, in the order the pattern binds it. The
28//! bindings are positions rather than names because that is what the walk has, and the rule
29//! carries the names for anything that has to say what it did. Building the replacement out of
30//! [`Piece`] belongs to the caller rather than to this file, because what a replacement becomes
31//! is a machine instruction in one crate and an IR instruction in the other, and this module is
32//! about matching.
33//!
34//! # A name written twice
35//!
36//! A pattern may write one name in two places, which is how `x & x` is said. The second place
37//! becomes [`Test::Same`] rather than a hole, and it asks the subject whether the two are the
38//! same thing rather than comparing nodes, because a node is a place and two places can hold one
39//! value. It is a concrete test, so it is tried before the wildcard for the same reason every
40//! other test is: a rule about one value in both operands is more specific than a rule about any
41//! two.
42//!
43//! # Order
44//!
45//! At every node the concrete tests are tried before the branch that takes anything, so a rule
46//! naming an operand is tried before a rule taking whatever is there. That is the maximal munch
47//! `spec/10-backend.md` asks for, and it falls out of the shape of the trie rather than being
48//! sorted for. Among rules that are equally specific the first one written wins.
49//!
50//! A guard is part of deciding whether a rule fires, so a rule whose guard is false is a rule
51//! that did not match, and the walk carries on looking rather than giving up. What that costs is
52//! the search from where the guard failed, which is the price of a guard being allowed to be
53//! about the values rather than only about the shape.
54
55/// The bits of a term the automaton asks about.
56///
57/// A node is whatever the thing doing the matching calls one of its terms: an IR value, an index
58/// into an arena, a pointer. It has to be cheap to copy because the walk keeps a stack of them.
59pub trait Subject {
60    /// What this subject calls one of its terms.
61    type Node: Copy;
62
63    /// The head of a term and how many arguments it has, or nothing if the term is not an
64    /// application. An IR instruction answers with its opcode and its width, spelled the way the
65    /// rule file spells it.
66    fn head(&self, node: Self::Node) -> Option<(&str, usize)>;
67
68    /// One argument of a term, counted from zero. Only ever asked for an argument the answer to
69    /// [`Subject::head`] said was there.
70    fn arg(&self, node: Self::Node, index: usize) -> Self::Node;
71
72    /// The value of a term that is a constant, or nothing if it is not one. This is what a
73    /// pattern matching a literal is asking, and what a guard reads.
74    fn int(&self, node: Self::Node) -> Option<i128>;
75
76    /// Whether two terms are the same thing, which is what a pattern that writes one name in two
77    /// places is asking.
78    ///
79    /// This is a question for the subject rather than something the walk can answer by comparing
80    /// nodes, because a node is a place and two places can hold one value. In
81    /// `(and.i32 (value.i32 x) (value.i32 x))` the two operands are operand zero and operand
82    /// one, which are different places, and what the rule wants to know is whether the same
83    /// value is in both. A subject that cannot tell may answer `false`, which costs the rule a
84    /// match it could have had and never gives it one it should not.
85    fn same(&self, a: Self::Node, b: Self::Node) -> bool;
86}
87
88/// One test on one subterm.
89#[derive(Debug)]
90pub enum Test {
91    /// The subterm has to be this head applied to this many arguments.
92    App {
93        /// The name in head position.
94        head: &'static str,
95        /// How many arguments it takes.
96        arity: usize,
97    },
98    /// The subterm has to be this constant.
99    Int(i128),
100    /// The subterm has to be the same thing as a binding this pattern already made, named by
101    /// which binding it is. A pattern writes one where it writes a name for the second time, so
102    /// this is how `x & x` is told apart from `x & y`.
103    Same(usize),
104}
105
106/// One node of the trie over the patterns.
107#[derive(Debug)]
108pub struct Node {
109    /// The tests to try, in the order the rules were written, before the wildcard.
110    pub tests: &'static [(Test, u32)],
111    /// The branch that takes anything, and the name the first rule to reach it gave that hole.
112    pub wildcard: Option<(&'static str, u32)>,
113    /// The rule that ends here, if one does.
114    pub accept: Option<u32>,
115}
116
117/// One piece of a replacement, in the pre-order that builds it.
118#[derive(Debug)]
119pub enum Piece {
120    /// Whatever the pattern bound at this position.
121    Var {
122        /// The name the rule gave it, for anything that has to say what it did.
123        name: &'static str,
124        /// Which binding of the match it is.
125        index: usize,
126    },
127    /// A constant written in the rule.
128    Int(i128),
129    /// A term the rule writes, which is an instruction once the caller has built it.
130    App {
131        /// The name in head position.
132        head: &'static str,
133        /// How many arguments it takes.
134        arity: usize,
135    },
136}
137
138/// A condition on the constants a pattern matched.
139///
140/// It is handed one entry per binding, holding the value of that binding when it has one. A
141/// guard about a binding that is not a constant is false, which is how a rule about a number
142/// declines an operand that is a register.
143pub type Guard = fn(&[Option<i128>]) -> bool;
144
145/// One rule, as much of it as matching needs.
146#[derive(Debug)]
147pub struct Rule {
148    /// The pattern as it is written in the rule file, for diagnostics and for tests.
149    pub pattern: &'static str,
150    /// What to put in the matched term's place, flattened into pre-order.
151    pub replacement: &'static [Piece],
152    /// The condition on the match, if the rule has one.
153    pub guard: Option<Guard>,
154    /// The line of the rule file this rule starts on.
155    pub line: u32,
156}
157
158impl Rule {
159    /// The head of the replacement, which is what this rule writes.
160    #[must_use]
161    pub fn head(&self) -> Option<&'static str> {
162        match self.replacement.first() {
163            Some(Piece::App { head, .. }) => Some(head),
164            _ => None,
165        }
166    }
167}
168
169/// A set of rules, as an automaton over their patterns.
170#[derive(Debug)]
171pub struct Table {
172    /// The rule file this was built from, so that anything said about a rule can name a file
173    /// somebody can open.
174    pub source: &'static str,
175    /// The trie. Node zero is the root.
176    pub nodes: &'static [Node],
177    /// The rules, in the order the file writes them.
178    pub rules: &'static [Rule],
179}
180
181/// What a successful match found.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Match<N> {
184    /// Which rule of the table fired.
185    pub rule: usize,
186    /// What the pattern bound, in the order it binds it.
187    pub bindings: Vec<N>,
188}
189
190impl Table {
191    /// The rule that fires on this term, and what it bound.
192    ///
193    /// The term is matched as a whole. Finding the terms in a function worth matching is the
194    /// caller's job and not this one's.
195    #[must_use]
196    pub fn find<S: Subject>(&self, subject: &S, term: S::Node) -> Option<Match<S::Node>> {
197        let mut bindings = Vec::new();
198        let rule = self.run(subject, 0, vec![term], &mut bindings)?;
199        Some(Match { rule, bindings })
200    }
201
202    /// The rule a match found, which is the one thing every caller wants out of it.
203    #[must_use]
204    pub fn rule<N>(&self, found: &Match<N>) -> &Rule {
205        &self.rules[found.rule]
206    }
207
208    /// Walk the trie and the subject together.
209    ///
210    /// `left` is the subterms still to be matched, innermost last, so that popping gives the
211    /// pre-order the patterns were flattened in.
212    fn run<S: Subject>(
213        &self,
214        subject: &S,
215        at: usize,
216        mut left: Vec<S::Node>,
217        bindings: &mut Vec<S::Node>,
218    ) -> Option<usize> {
219        let Some(term) = left.pop() else {
220            return self.accept(subject, at, bindings);
221        };
222        let node = &self.nodes[at];
223        let head = subject.head(term);
224
225        for (test, next) in node.tests {
226            let matched = match test {
227                Test::Int(want) => subject.int(term) == Some(*want),
228                Test::App { head: want, arity } => {
229                    head.is_some_and(|(have, count)| have == *want && count == *arity)
230                }
231                // The binding is always there, because a pattern only writes a name for the
232                // second time after it has written it once and the trie keeps that order.
233                Test::Same(index) => {
234                    bindings.get(*index).is_some_and(|&bound| subject.same(bound, term))
235                }
236            };
237            if !matched {
238                continue;
239            }
240            let mut deeper = left.clone();
241            if let Some((_, arity)) = head {
242                for index in (0..arity).rev() {
243                    deeper.push(subject.arg(term, index));
244                }
245            }
246            let depth = bindings.len();
247            if let Some(rule) = self.run(subject, *next as usize, deeper, bindings) {
248                return Some(rule);
249            }
250            bindings.truncate(depth);
251        }
252
253        // The wildcard is last, which is the whole of what specificity order means here.
254        let (_, next) = node.wildcard.as_ref()?;
255        let depth = bindings.len();
256        bindings.push(term);
257        if let Some(rule) = self.run(subject, *next as usize, left, bindings) {
258            return Some(rule);
259        }
260        bindings.truncate(depth);
261        None
262    }
263
264    /// The rule that ends at this node, if one does and if its guard holds.
265    fn accept<S: Subject>(&self, subject: &S, at: usize, bindings: &[S::Node]) -> Option<usize> {
266        let rule = self.nodes[at].accept? as usize;
267        if let Some(guard) = self.rules[rule].guard {
268            // The values are collected here rather than as the bindings are made, because most
269            // rules have no guard and would pay for it every time.
270            let values: Vec<Option<i128>> =
271                bindings.iter().map(|&node| subject.int(node)).collect();
272            if !guard(&values) {
273                return None;
274            }
275        }
276        Some(rule)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::{Match, Node, Piece, Rule, Subject, Table, Test};
283
284    /// A term, in the only shape a test needs: a flat arena, because that is the shape the IR
285    /// has and answering the questions out of one is what the callers will be doing.
286    #[derive(Debug)]
287    enum Held {
288        Int(i128),
289        App(String, Vec<usize>),
290    }
291
292    #[derive(Debug, Default)]
293    struct Terms {
294        nodes: Vec<Held>,
295    }
296
297    impl Terms {
298        fn constant(&mut self, value: i128) -> usize {
299            self.nodes.push(Held::Int(value));
300            self.nodes.len() - 1
301        }
302
303        fn app(&mut self, head: &str, args: &[usize]) -> usize {
304            self.nodes.push(Held::App(head.to_owned(), args.to_vec()));
305            self.nodes.len() - 1
306        }
307    }
308
309    impl Subject for Terms {
310        type Node = usize;
311
312        fn head(&self, node: usize) -> Option<(&str, usize)> {
313            match &self.nodes[node] {
314                Held::App(head, args) => Some((head.as_str(), args.len())),
315                Held::Int(_) => None,
316            }
317        }
318
319        fn arg(&self, node: usize, index: usize) -> usize {
320            match &self.nodes[node] {
321                Held::App(_, args) => args[index],
322                Held::Int(_) => unreachable!("a constant has no arguments"),
323            }
324        }
325
326        fn int(&self, node: usize) -> Option<i128> {
327            match self.nodes[node] {
328                Held::Int(value) => Some(value),
329                Held::App(..) => None,
330            }
331        }
332
333        // An index into the arena is the identity of a term here, so two places are the same
334        // thing when they point at the same entry. A subject over the IR answers this out of the
335        // value each place holds instead, which is the same question asked of a different shape.
336        fn same(&self, a: usize, b: usize) -> bool {
337            a == b
338        }
339    }
340
341    /// A table written by hand, in the shape `rucc-rules` emits.
342    ///
343    /// Two rules over `(add x k)`: the first wants the constant to be zero and the second takes
344    /// any constant that is not negative. That is enough to exercise everything the walk does,
345    /// which is a concrete test before a wildcard, a guard that can refuse, and the search
346    /// carrying on after it does. A third rule, `(and x x)`, is the one that writes a name
347    /// twice.
348    static NODES: &[Node] = &[
349        // 0, the root.
350        Node {
351            tests: &[
352                (Test::App { head: "add", arity: 2 }, 1),
353                (Test::App { head: "and", arity: 2 }, 5),
354            ],
355            wildcard: None,
356            accept: None,
357        },
358        // 1, the first operand.
359        Node { tests: &[], wildcard: Some(("x", 2)), accept: None },
360        // 2, the second operand.
361        Node { tests: &[(Test::Int(0), 3)], wildcard: Some(("k", 4)), accept: None },
362        // 3, an addition of zero.
363        Node { tests: &[], wildcard: None, accept: Some(0) },
364        // 4, an addition of anything, if the guard holds.
365        Node { tests: &[], wildcard: None, accept: Some(1) },
366        // 5, the first operand of the conjunction, which is the one that binds.
367        Node { tests: &[], wildcard: Some(("x", 6)), accept: None },
368        // 6, the second operand, which has to be what the first one bound.
369        Node { tests: &[(Test::Same(0), 7)], wildcard: None, accept: None },
370        // 7, a conjunction of one thing with itself.
371        Node { tests: &[], wildcard: None, accept: Some(2) },
372    ];
373
374    fn not_negative(bound: &[Option<i128>]) -> bool {
375        let Some(Some(k)) = bound.get(1).copied() else { return false };
376        k >= 0
377    }
378
379    static RULES: &[Rule] = &[
380        Rule {
381            pattern: "(add x 0)",
382            replacement: &[Piece::Var { name: "x", index: 0 }],
383            guard: None,
384            line: 1,
385        },
386        Rule {
387            pattern: "(add x k)",
388            replacement: &[
389                Piece::App { head: "add_immediate", arity: 2 },
390                Piece::Var { name: "x", index: 0 },
391                Piece::Var { name: "k", index: 1 },
392            ],
393            guard: Some(not_negative),
394            line: 2,
395        },
396        Rule {
397            pattern: "(and x x)",
398            replacement: &[Piece::Var { name: "x", index: 0 }],
399            guard: None,
400            line: 3,
401        },
402    ];
403
404    static TABLE: Table = Table { source: "rules/test.rules", nodes: NODES, rules: RULES };
405
406    fn add(terms: &mut Terms, second: usize) -> usize {
407        let first = terms.app("v0", &[]);
408        terms.app("add", &[first, second])
409    }
410
411    /// The concrete test is tried before the wildcard, so the rule about zero wins over the rule
412    /// about any constant even though both of them match. That is the whole of what specificity
413    /// order means here, and it falls out of the shape of the trie.
414    #[test]
415    fn the_rule_that_names_the_operand_beats_the_rule_that_takes_anything() {
416        let mut terms = Terms::default();
417        let zero = terms.constant(0);
418        let term = add(&mut terms, zero);
419        let found = TABLE.find(&terms, term).expect("a rule fires");
420        assert_eq!(TABLE.rule(&found).pattern, "(add x 0)");
421    }
422
423    /// The bindings come back in the order the pattern binds them, which is the pre-order the
424    /// replacement was flattened in, so a `Piece::Var` can be read as an index into them.
425    #[test]
426    fn a_match_gives_back_what_the_pattern_bound_in_the_order_it_bound_it() {
427        let mut terms = Terms::default();
428        let seven = terms.constant(7);
429        let term = add(&mut terms, seven);
430        let found = TABLE.find(&terms, term).expect("a rule fires");
431        let rule = TABLE.rule(&found);
432        assert_eq!(rule.pattern, "(add x k)");
433        assert_eq!(rule.head(), Some("add_immediate"));
434        assert_eq!(found.bindings.len(), 2);
435        assert_eq!(found.bindings[1], seven);
436        assert_eq!(terms.int(found.bindings[1]), Some(7));
437    }
438
439    /// A guard that does not hold is a rule that did not match, and there is nothing else to
440    /// try, so the answer is nothing rather than the wrong rule.
441    #[test]
442    fn a_guard_that_refuses_takes_its_rule_out_of_the_running() {
443        let mut terms = Terms::default();
444        let negative = terms.constant(-1);
445        let term = add(&mut terms, negative);
446        assert_eq!(TABLE.find(&terms, term), None);
447    }
448
449    /// The same guard against an operand that is not a constant at all. A guard is a claim about
450    /// a number, so a register makes it false rather than an error.
451    #[test]
452    fn a_guard_about_a_number_refuses_an_operand_that_is_not_one() {
453        let mut terms = Terms::default();
454        let other = terms.app("v1", &[]);
455        let term = add(&mut terms, other);
456        assert_eq!(TABLE.find(&terms, term), None);
457    }
458
459    #[test]
460    fn a_term_no_rule_covers_finds_no_rule() {
461        let mut terms = Terms::default();
462        let x = terms.app("v0", &[]);
463        let y = terms.app("v1", &[]);
464        let term = terms.app("no.such.head", &[x, y]);
465        assert_eq!(TABLE.find(&terms, term), None);
466    }
467
468    /// The rule that writes one name twice. Both operands are the same term, so the test that
469    /// they are holds and the rule fires, and what comes back is the one binding the pattern
470    /// made rather than two.
471    #[test]
472    fn a_pattern_that_names_one_hole_twice_matches_a_term_that_has_one_thing_in_both() {
473        let mut terms = Terms::default();
474        let x = terms.app("v0", &[]);
475        let term = terms.app("and", &[x, x]);
476        let found = TABLE.find(&terms, term).expect("a rule fires");
477        assert_eq!(TABLE.rule(&found).pattern, "(and x x)");
478        assert_eq!(found.bindings, vec![x]);
479    }
480
481    /// The same rule against two different terms. There is no wildcard beside the test, so a
482    /// conjunction of two things is a conjunction no rule covers rather than one this rule
483    /// wrongly claims.
484    #[test]
485    fn a_pattern_that_names_one_hole_twice_refuses_a_term_that_has_two_things_in_it() {
486        let mut terms = Terms::default();
487        let x = terms.app("v0", &[]);
488        let y = terms.app("v1", &[]);
489        let term = terms.app("and", &[x, y]);
490        assert_eq!(TABLE.find(&terms, term), None);
491    }
492
493    /// A match is what a caller keeps, so it says what it is when a test prints it.
494    #[test]
495    fn a_match_names_the_rule_it_found() {
496        let mut terms = Terms::default();
497        let zero = terms.constant(0);
498        let term = add(&mut terms, zero);
499        assert_eq!(TABLE.find(&terms, term), Some(Match { rule: 0, bindings: vec![term - 1] }));
500    }
501}