Skip to main content

rucc_codegen/
select.rs

1//! Matching a target's lowering rules against a term.
2//!
3//! Design: `spec/10-backend.md` section 10.2. The rules themselves are in `rules/`, one file per
4//! target, and the automaton they compile into is generated by `rucc-rules` when this crate is
5//! built. What is here is the walk over that automaton, which is the same walk for every target
6//! and is written once.
7//!
8//! # What a subject is
9//!
10//! A rule matches a term, and the compiler does not have terms: it has a function full of
11//! instructions, and what a pattern is about is one of them and whatever it was computed from.
12//! So the walk is written against [`Subject`], which is the three questions the automaton asks
13//! of whatever it is matching, and the selector answers them out of the IR without building a
14//! term to be thrown away. A test can answer them out of anything at all, which is what the
15//! tests at the bottom of this file do.
16//!
17//! # What a match gives back
18//!
19//! The rule that fired and what its pattern bound, in the order the pattern binds it. The
20//! bindings are positions rather than names because that is what the walk has, and the rule
21//! carries the names for anything that has to say what it did. Building the replacement out of
22//! [`Piece`] is the selector's job rather than this file's, because what a machine term becomes
23//! is a machine instruction, and this module is about matching.
24//!
25//! # Order
26//!
27//! At every node the concrete tests are tried before the branch that takes anything, so a rule
28//! naming an operand is tried before a rule taking whatever is there. That is the maximal munch
29//! `spec/10-backend.md` asks for, and it falls out of the shape of the trie rather than being
30//! sorted for. Among rules that are equally specific the first one written wins.
31//!
32//! A guard is part of deciding whether a rule fires, so a rule whose guard is false is a rule
33//! that did not match, and the walk carries on looking rather than giving up. What that costs is
34//! the search from where the guard failed, which is the price of a guard being allowed to be
35//! about the values rather than only about the shape.
36
37pub mod x86_64;
38
39/// The bits of a term the automaton asks about.
40///
41/// A node is whatever the thing doing the matching calls one of its terms: an IR value, an index
42/// into an arena, a pointer. It has to be cheap to copy because the walk keeps a stack of them.
43pub trait Subject {
44    /// What this subject calls one of its terms.
45    type Node: Copy;
46
47    /// The head of a term and how many arguments it has, or nothing if the term is not an
48    /// application. An IR instruction answers with its opcode and its width, spelled the way the
49    /// rule file spells it.
50    fn head(&self, node: Self::Node) -> Option<(&str, usize)>;
51
52    /// One argument of a term, counted from zero. Only ever asked for an argument the answer to
53    /// [`Subject::head`] said was there.
54    fn arg(&self, node: Self::Node, index: usize) -> Self::Node;
55
56    /// The value of a term that is a constant, or nothing if it is not one. This is what a
57    /// pattern matching a literal is asking, and what a guard reads.
58    fn int(&self, node: Self::Node) -> Option<i128>;
59}
60
61/// One test on one subterm.
62#[derive(Debug)]
63pub enum Test {
64    /// The subterm has to be this head applied to this many arguments.
65    App {
66        /// The name in head position.
67        head: &'static str,
68        /// How many arguments it takes.
69        arity: usize,
70    },
71    /// The subterm has to be this constant.
72    Int(i128),
73}
74
75/// One node of the trie over the patterns.
76#[derive(Debug)]
77pub struct Node {
78    /// The tests to try, in the order the rules were written, before the wildcard.
79    pub tests: &'static [(Test, u32)],
80    /// The branch that takes anything, and the name the first rule to reach it gave that hole.
81    pub wildcard: Option<(&'static str, u32)>,
82    /// The rule that ends here, if one does.
83    pub accept: Option<u32>,
84}
85
86/// One piece of a replacement, in the pre-order that builds it.
87#[derive(Debug)]
88pub enum Piece {
89    /// Whatever the pattern bound at this position.
90    Var {
91        /// The name the rule gave it, for anything that has to say what it did.
92        name: &'static str,
93        /// Which binding of the match it is.
94        index: usize,
95    },
96    /// A constant written in the rule.
97    Int(i128),
98    /// A machine term, which is an instruction once the selector has built it.
99    App {
100        /// The name in head position.
101        head: &'static str,
102        /// How many arguments it takes.
103        arity: usize,
104    },
105}
106
107/// A condition on the constants a pattern matched.
108///
109/// It is handed one entry per binding, holding the value of that binding when it has one. A
110/// guard about a binding that is not a constant is false, which is how a rule about a number
111/// declines an operand that is a register.
112pub type Guard = fn(&[Option<i128>]) -> bool;
113
114/// One lowering rule, as much of it as matching needs.
115#[derive(Debug)]
116pub struct Rule {
117    /// The pattern as it is written in the rule file, for diagnostics and for tests.
118    pub pattern: &'static str,
119    /// What to put in the matched term's place, flattened into pre-order.
120    pub replacement: &'static [Piece],
121    /// The condition on the match, if the rule has one.
122    pub guard: Option<Guard>,
123    /// The line of the rule file this rule starts on.
124    pub line: u32,
125}
126
127impl Rule {
128    /// The head of the replacement, which is the instruction this rule selects.
129    #[must_use]
130    pub fn head(&self) -> Option<&'static str> {
131        match self.replacement.first() {
132            Some(Piece::App { head, .. }) => Some(head),
133            _ => None,
134        }
135    }
136}
137
138/// A target's lowering rules, as an automaton over their patterns.
139#[derive(Debug)]
140pub struct Table {
141    /// The rule file this was built from, so that anything said about a rule can name a file
142    /// somebody can open.
143    pub source: &'static str,
144    /// The trie. Node zero is the root.
145    pub nodes: &'static [Node],
146    /// The rules, in the order the file writes them.
147    pub rules: &'static [Rule],
148}
149
150/// What a successful match found.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Match<N> {
153    /// Which rule of the table fired.
154    pub rule: usize,
155    /// What the pattern bound, in the order it binds it.
156    pub bindings: Vec<N>,
157}
158
159impl Table {
160    /// The rule that fires on this term, and what it bound.
161    ///
162    /// The term is matched as a whole. Finding the terms in a function worth matching is the
163    /// selector's job and not this one's.
164    #[must_use]
165    pub fn find<S: Subject>(&self, subject: &S, term: S::Node) -> Option<Match<S::Node>> {
166        let mut bindings = Vec::new();
167        let rule = self.run(subject, 0, vec![term], &mut bindings)?;
168        Some(Match { rule, bindings })
169    }
170
171    /// The rule a match found, which is the one thing every caller wants out of it.
172    #[must_use]
173    pub fn rule<N>(&self, found: &Match<N>) -> &Rule {
174        &self.rules[found.rule]
175    }
176
177    /// Walk the trie and the subject together.
178    ///
179    /// `left` is the subterms still to be matched, innermost last, so that popping gives the
180    /// pre-order the patterns were flattened in.
181    fn run<S: Subject>(
182        &self,
183        subject: &S,
184        at: usize,
185        mut left: Vec<S::Node>,
186        bindings: &mut Vec<S::Node>,
187    ) -> Option<usize> {
188        let Some(term) = left.pop() else {
189            return self.accept(subject, at, bindings);
190        };
191        let node = &self.nodes[at];
192        let head = subject.head(term);
193
194        for (test, next) in node.tests {
195            let matched = match test {
196                Test::Int(want) => subject.int(term) == Some(*want),
197                Test::App { head: want, arity } => {
198                    head.is_some_and(|(have, count)| have == *want && count == *arity)
199                }
200            };
201            if !matched {
202                continue;
203            }
204            let mut deeper = left.clone();
205            if let Some((_, arity)) = head {
206                for index in (0..arity).rev() {
207                    deeper.push(subject.arg(term, index));
208                }
209            }
210            let depth = bindings.len();
211            if let Some(rule) = self.run(subject, *next as usize, deeper, bindings) {
212                return Some(rule);
213            }
214            bindings.truncate(depth);
215        }
216
217        // The wildcard is last, which is the whole of what specificity order means here.
218        let (_, next) = node.wildcard.as_ref()?;
219        let depth = bindings.len();
220        bindings.push(term);
221        if let Some(rule) = self.run(subject, *next as usize, left, bindings) {
222            return Some(rule);
223        }
224        bindings.truncate(depth);
225        None
226    }
227
228    /// The rule that ends at this node, if one does and if its guard holds.
229    fn accept<S: Subject>(&self, subject: &S, at: usize, bindings: &[S::Node]) -> Option<usize> {
230        let rule = self.nodes[at].accept? as usize;
231        if let Some(guard) = self.rules[rule].guard {
232            // The values are collected here rather than as the bindings are made, because most
233            // rules have no guard and would pay for it every time.
234            let values: Vec<Option<i128>> =
235                bindings.iter().map(|&node| subject.int(node)).collect();
236            if !guard(&values) {
237                return None;
238            }
239        }
240        Some(rule)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::x86_64::TABLE;
247    use super::{Piece, Subject};
248
249    /// A term, in the only shape a test needs: a flat arena, because that is the shape the IR
250    /// has and answering the questions out of one is what the selector will be doing.
251    #[derive(Debug)]
252    enum Node {
253        Int(i128),
254        App(String, Vec<usize>),
255    }
256
257    #[derive(Debug, Default)]
258    struct Terms {
259        nodes: Vec<Node>,
260    }
261
262    impl Terms {
263        fn constant(&mut self, value: i128) -> usize {
264            self.nodes.push(Node::Int(value));
265            self.nodes.len() - 1
266        }
267
268        fn app(&mut self, head: &str, args: &[usize]) -> usize {
269            self.nodes.push(Node::App(head.to_owned(), args.to_vec()));
270            self.nodes.len() - 1
271        }
272
273        /// A register operand, which is a term with a head the rules write and nothing under it.
274        fn value(&mut self, width: u32, name: &str) -> usize {
275            let inner = self.app(name, &[]);
276            self.app(&format!("value.i{width}"), &[inner])
277        }
278    }
279
280    impl Subject for Terms {
281        type Node = usize;
282
283        fn head(&self, node: usize) -> Option<(&str, usize)> {
284            match &self.nodes[node] {
285                Node::App(head, args) => Some((head.as_str(), args.len())),
286                Node::Int(_) => None,
287            }
288        }
289
290        fn arg(&self, node: usize, index: usize) -> usize {
291            match &self.nodes[node] {
292                Node::App(_, args) => args[index],
293                Node::Int(_) => unreachable!("a constant has no arguments"),
294            }
295        }
296
297        fn int(&self, node: usize) -> Option<i128> {
298            match self.nodes[node] {
299                Node::Int(value) => Some(value),
300                Node::App(..) => None,
301            }
302        }
303    }
304
305    /// What the head of the rule that fired selects, which is the answer every one of these
306    /// tests is really about.
307    fn selects(terms: &Terms, term: usize) -> Option<&'static str> {
308        let found = TABLE.find(terms, term)?;
309        TABLE.rule(&found).head()
310    }
311
312    #[test]
313    fn the_table_holds_every_rule_the_file_writes() {
314        let text = include_str!("../rules/x86-64.rules");
315        let written = text.lines().filter(|line| line.starts_with("(rule ")).count();
316        assert_eq!(TABLE.rules.len(), written, "the table and the rule file disagree");
317        assert_eq!(TABLE.source, "rules/x86-64.rules");
318    }
319
320    #[test]
321    fn an_addition_of_two_registers_is_the_register_form() {
322        let mut terms = Terms::default();
323        let x = terms.value(64, "v0");
324        let y = terms.value(64, "v1");
325        let add = terms.app("add.i64", &[x, y]);
326        assert_eq!(selects(&terms, add), Some("x64.add_rr_64"));
327    }
328
329    /// The bindings are the operands in the order the pattern names them, and the replacement
330    /// says which of them goes where. This is the whole of what the selector will read.
331    ///
332    /// What a name is bound to is what the pattern put it under, so `(value.i32 x)` binds the
333    /// register and not the term saying it is one. That is the difference between the operand of
334    /// the instruction this becomes and a wrapper that exists to say how wide it is.
335    #[test]
336    fn a_match_gives_back_the_operands_the_pattern_named() {
337        let mut terms = Terms::default();
338        let first = terms.app("v0", &[]);
339        let second = terms.app("v1", &[]);
340        let x = terms.app("value.i32", &[first]);
341        let y = terms.app("value.i32", &[second]);
342        let sub = terms.app("sub.i32", &[x, y]);
343        let found = TABLE.find(&terms, sub).expect("a rule fires");
344        let rule = TABLE.rule(&found);
345        assert_eq!(rule.pattern, "(sub.i32 (value.i32 x) (value.i32 y))");
346        assert_eq!(found.bindings, vec![first, second]);
347        let names: Vec<&str> = rule
348            .replacement
349            .iter()
350            .filter_map(|piece| match piece {
351                Piece::Var { name, index } => {
352                    assert_eq!(found.bindings[*index], if *index == 0 { first } else { second });
353                    Some(*name)
354                }
355                _ => None,
356            })
357            .collect();
358        assert_eq!(names, ["x", "y"]);
359    }
360
361    /// An immediate the instruction has room for takes the immediate form. The rule for it is
362    /// guarded, so this is also the test that a guard which holds does not stop a rule firing.
363    #[test]
364    fn an_addition_of_an_immediate_that_fits_is_the_immediate_form() {
365        let mut terms = Terms::default();
366        let x = terms.value(64, "v0");
367        let k = terms.constant(4);
368        let k = terms.app("iconst.i64", &[k]);
369        let add = terms.app("add.i64", &[x, k]);
370        assert_eq!(selects(&terms, add), Some("x64.add_ri_64"));
371    }
372
373    /// An immediate too wide for the encoding is what the guard is there to refuse. Nothing else
374    /// matches such a term, and that is the right answer: the constant has to be put in a
375    /// register first, which is a decision for the selector and not for the table.
376    #[test]
377    fn an_addition_of_an_immediate_too_wide_for_the_form_matches_nothing() {
378        let mut terms = Terms::default();
379        let x = terms.value(64, "v0");
380        let k = terms.constant(1 << 40);
381        let k = terms.app("iconst.i64", &[k]);
382        let add = terms.app("add.i64", &[x, k]);
383        assert_eq!(selects(&terms, add), None);
384    }
385
386    /// The other shape of guard, which is a shift count the width allows.
387    #[test]
388    fn a_shift_by_a_count_the_width_allows_is_the_immediate_form() {
389        let mut terms = Terms::default();
390        let x = terms.value(64, "v0");
391        let k = terms.constant(3);
392        let k = terms.app("iconst.i64", &[k]);
393        let shl = terms.app("shl.i64", &[x, k]);
394        assert_eq!(selects(&terms, shl), Some("x64.shl_ri_64"));
395    }
396
397    #[test]
398    fn a_shift_by_a_count_the_width_does_not_allow_matches_nothing() {
399        let mut terms = Terms::default();
400        let x = terms.value(64, "v0");
401        let k = terms.constant(64);
402        let k = terms.app("iconst.i64", &[k]);
403        let shl = terms.app("shl.i64", &[x, k]);
404        assert_eq!(selects(&terms, shl), None);
405    }
406
407    /// A term the rule set says nothing about is nothing rather than a wrong answer, which is
408    /// what the completeness check in `spec/10-backend.md` will be for.
409    #[test]
410    fn a_term_no_rule_covers_finds_no_rule() {
411        let mut terms = Terms::default();
412        let x = terms.value(64, "v0");
413        let y = terms.value(64, "v1");
414        let odd = terms.app("no.such.opcode", &[x, y]);
415        assert_eq!(selects(&terms, odd), None);
416    }
417}