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.
6//!
7//! The walk over that automaton is [`rucc_base::rules`], because `rucc-opt` matches IR against a
8//! table of rewrite rules with the same walk and neither crate can see the other. What is here
9//! is which targets there are and the tests that the x86-64 table lowers what it should.
10//!
11//! The names are re-exported rather than reached for through `rucc_base`, because the generated
12//! file refers to them through `super` and that is the whole of the contract between the two.
13
14pub mod x86_64;
15
16pub use rucc_base::rules::{Guard, Match, Node, Piece, Rule, Subject, Table};
17
18#[cfg(test)]
19mod tests {
20    use super::x86_64::TABLE;
21    use super::{Piece, Subject};
22
23    /// A term, in the only shape a test needs: a flat arena, because that is the shape the IR
24    /// has and answering the questions out of one is what the selector will be doing.
25    #[derive(Debug)]
26    enum Node {
27        Int(i128),
28        App(String, Vec<usize>),
29    }
30
31    #[derive(Debug, Default)]
32    struct Terms {
33        nodes: Vec<Node>,
34    }
35
36    impl Terms {
37        fn constant(&mut self, value: i128) -> usize {
38            self.nodes.push(Node::Int(value));
39            self.nodes.len() - 1
40        }
41
42        fn app(&mut self, head: &str, args: &[usize]) -> usize {
43            self.nodes.push(Node::App(head.to_owned(), args.to_vec()));
44            self.nodes.len() - 1
45        }
46
47        /// A register operand, which is a term with a head the rules write and nothing under it.
48        fn value(&mut self, width: u32, name: &str) -> usize {
49            let inner = self.app(name, &[]);
50            self.app(&format!("value.i{width}"), &[inner])
51        }
52    }
53
54    impl Subject for Terms {
55        type Node = usize;
56
57        fn head(&self, node: usize) -> Option<(&str, usize)> {
58            match &self.nodes[node] {
59                Node::App(head, args) => Some((head.as_str(), args.len())),
60                Node::Int(_) => None,
61            }
62        }
63
64        fn arg(&self, node: usize, index: usize) -> usize {
65            match &self.nodes[node] {
66                Node::App(_, args) => args[index],
67                Node::Int(_) => unreachable!("a constant has no arguments"),
68            }
69        }
70
71        fn int(&self, node: usize) -> Option<i128> {
72            match self.nodes[node] {
73                Node::Int(value) => Some(value),
74                Node::App(..) => None,
75            }
76        }
77
78        // An index into the arena is the identity of a term here, so two places are the same
79        // thing when they point at the same entry.
80        fn same(&self, a: usize, b: usize) -> bool {
81            a == b
82        }
83    }
84
85    /// What the head of the rule that fired selects, which is the answer every one of these
86    /// tests is really about.
87    fn selects(terms: &Terms, term: usize) -> Option<&'static str> {
88        let found = TABLE.find(terms, term)?;
89        TABLE.rule(&found).head()
90    }
91
92    /// No pattern is reached by reading past the ones in front of it.
93    ///
94    /// `spec/optimizer/36-lowering-and-isel.md` section 36.5 asks for the decision to be on the
95    /// shape of the term, and the root of this table is where that is worth anything: every
96    /// instruction the selector looks at arrives there, and a hundred and sixty seven different
97    /// heads are written on it. Sorted, that is eight comparisons and the walk finds the branch.
98    /// In the order the rules happen to be written it would be a hundred and sixty seven, every
99    /// time, and worst for the terms no rule covers, which are the ones the selector has to see
100    /// the most of.
101    ///
102    /// What is asserted is the property the search needs, which is that every node is in order.
103    /// A node that is not is not a slower table, it is a wrong one, because a binary search over
104    /// an unsorted list finds nothing and the rule silently stops firing.
105    #[test]
106    fn no_rule_is_reached_by_reading_past_the_rules_in_front_of_it() {
107        let root = TABLE.nodes.first().expect("the table has a root");
108        assert!(root.heads.len() > 100, "the root is the node this is about");
109        for (at, node) in TABLE.nodes.iter().enumerate() {
110            assert!(node.heads.is_sorted(), "node {at} is not in an order a search can use");
111            assert!(node.ints.is_sorted(), "node {at} is not in an order a search can use");
112        }
113    }
114
115    #[test]
116    fn the_table_holds_every_rule_the_file_writes() {
117        let text = include_str!("../rules/x86-64.rules");
118        let written = text.lines().filter(|line| line.starts_with("(rule ")).count();
119        assert_eq!(TABLE.rules.len(), written, "the table and the rule file disagree");
120        assert_eq!(TABLE.source, "rules/x86-64.rules");
121    }
122
123    #[test]
124    fn an_addition_of_two_registers_is_the_register_form() {
125        let mut terms = Terms::default();
126        let x = terms.value(64, "v0");
127        let y = terms.value(64, "v1");
128        let add = terms.app("add.i64", &[x, y]);
129        assert_eq!(selects(&terms, add), Some("x64.add_rr_64"));
130    }
131
132    /// The bindings are the operands in the order the pattern names them, and the replacement
133    /// says which of them goes where. This is the whole of what the selector will read.
134    ///
135    /// What a name is bound to is what the pattern put it under, so `(value.i32 x)` binds the
136    /// register and not the term saying it is one. That is the difference between the operand of
137    /// the instruction this becomes and a wrapper that exists to say how wide it is.
138    #[test]
139    fn a_match_gives_back_the_operands_the_pattern_named() {
140        let mut terms = Terms::default();
141        let first = terms.app("v0", &[]);
142        let second = terms.app("v1", &[]);
143        let x = terms.app("value.i32", &[first]);
144        let y = terms.app("value.i32", &[second]);
145        let sub = terms.app("sub.i32", &[x, y]);
146        let found = TABLE.find(&terms, sub).expect("a rule fires");
147        let rule = TABLE.rule(&found);
148        assert_eq!(rule.pattern, "(sub.i32 (value.i32 x) (value.i32 y))");
149        assert_eq!(found.bindings, vec![first, second]);
150        let names: Vec<&str> = rule
151            .replacement
152            .iter()
153            .filter_map(|piece| match piece {
154                Piece::Var { name, index } => {
155                    assert_eq!(found.bindings[*index], if *index == 0 { first } else { second });
156                    Some(*name)
157                }
158                _ => None,
159            })
160            .collect();
161        assert_eq!(names, ["x", "y"]);
162    }
163
164    /// An immediate the instruction has room for takes the immediate form. The rule for it is
165    /// guarded, so this is also the test that a guard which holds does not stop a rule firing.
166    #[test]
167    fn an_addition_of_an_immediate_that_fits_is_the_immediate_form() {
168        let mut terms = Terms::default();
169        let x = terms.value(64, "v0");
170        let k = terms.constant(4);
171        let k = terms.app("iconst.i64", &[k]);
172        let add = terms.app("add.i64", &[x, k]);
173        assert_eq!(selects(&terms, add), Some("x64.add_ri_64"));
174    }
175
176    /// An immediate too wide for the encoding is what the guard is there to refuse. Nothing else
177    /// matches such a term, and that is the right answer: the constant has to be put in a
178    /// register first, which is a decision for the selector and not for the table.
179    #[test]
180    fn an_addition_of_an_immediate_too_wide_for_the_form_matches_nothing() {
181        let mut terms = Terms::default();
182        let x = terms.value(64, "v0");
183        let k = terms.constant(1 << 40);
184        let k = terms.app("iconst.i64", &[k]);
185        let add = terms.app("add.i64", &[x, k]);
186        assert_eq!(selects(&terms, add), None);
187    }
188
189    /// The other shape of guard, which is a shift count the width allows.
190    #[test]
191    fn a_shift_by_a_count_the_width_allows_is_the_immediate_form() {
192        let mut terms = Terms::default();
193        let x = terms.value(64, "v0");
194        let k = terms.constant(3);
195        let k = terms.app("iconst.i64", &[k]);
196        let shl = terms.app("shl.i64", &[x, k]);
197        assert_eq!(selects(&terms, shl), Some("x64.shl_ri_64"));
198    }
199
200    #[test]
201    fn a_shift_by_a_count_the_width_does_not_allow_matches_nothing() {
202        let mut terms = Terms::default();
203        let x = terms.value(64, "v0");
204        let k = terms.constant(64);
205        let k = terms.app("iconst.i64", &[k]);
206        let shl = terms.app("shl.i64", &[x, k]);
207        assert_eq!(selects(&terms, shl), None);
208    }
209
210    /// One bit reaches the byte instructions, which is the whole of how the machine holds a truth
211    /// value. The widening is the interesting one: it is `movzbl` under a name of its own, so the
212    /// rule that fires here is not the rule a byte would have found.
213    #[test]
214    fn a_truth_value_is_lowered_to_the_byte_instructions_that_keep_it_one() {
215        let mut terms = Terms::default();
216        let x = terms.value(1, "v0");
217        let y = terms.value(1, "v1");
218        let xor = terms.app("xor.i1", &[x, y]);
219        assert_eq!(selects(&terms, xor), Some("x64.xor_rr_8"));
220
221        let x = terms.value(1, "v2");
222        let wide = terms.app("zext.i1.i32", &[x]);
223        assert_eq!(selects(&terms, wide), Some("x64.bit_to_32"));
224
225        let x = terms.value(8, "v3");
226        let byte = terms.app("zext.i8.i32", &[x]);
227        assert_eq!(selects(&terms, byte), Some("x64.movzx_8_32"));
228    }
229
230    /// The half of a truth value that is an object rather than a value in a register. A `_Bool`
231    /// in memory is a byte holding a zero or a one, so a load widens on the way in and a store
232    /// writes the byte, and both are named apart from the byte pair for the reason the widening
233    /// is named apart from the byte widening. The narrowing is the mask, and it is the one of
234    /// these that nothing in C asks for directly: a bit field one bit wide whose type is a
235    /// `_Bool` is what writes it.
236    #[test]
237    fn a_truth_value_in_memory_is_the_byte_it_lives_in() {
238        let mut terms = Terms::default();
239        let address = terms.value(64, "v0");
240        let read = terms.app("load.i1", &[address]);
241        assert_eq!(selects(&terms, read), Some("x64.mov_rm_bit"));
242
243        let value = terms.value(1, "v1");
244        let address = terms.value(64, "v2");
245        let write = terms.app("store.i1", &[value, address]);
246        assert_eq!(selects(&terms, write), Some("x64.mov_mr_bit"));
247
248        let value = terms.value(1, "v3");
249        let back = terms.app("ret.i1", &[value]);
250        assert_eq!(selects(&terms, back), Some("x64.ret_val_8"));
251
252        let x = terms.value(32, "v4");
253        let bit = terms.app("trunc.i32.i1", &[x]);
254        assert_eq!(selects(&terms, bit), Some("x64.bit_of_32"));
255    }
256
257    /// A term the rule set says nothing about is nothing rather than a wrong answer, which is
258    /// what the completeness check in `spec/10-backend.md` will be for.
259    #[test]
260    fn a_term_no_rule_covers_finds_no_rule() {
261        let mut terms = Terms::default();
262        let x = terms.value(64, "v0");
263        let y = terms.value(64, "v1");
264        let odd = terms.app("no.such.opcode", &[x, y]);
265        assert_eq!(selects(&terms, odd), None);
266    }
267}