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}