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, Test};
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    #[test]
93    fn the_table_holds_every_rule_the_file_writes() {
94        let text = include_str!("../rules/x86-64.rules");
95        let written = text.lines().filter(|line| line.starts_with("(rule ")).count();
96        assert_eq!(TABLE.rules.len(), written, "the table and the rule file disagree");
97        assert_eq!(TABLE.source, "rules/x86-64.rules");
98    }
99
100    #[test]
101    fn an_addition_of_two_registers_is_the_register_form() {
102        let mut terms = Terms::default();
103        let x = terms.value(64, "v0");
104        let y = terms.value(64, "v1");
105        let add = terms.app("add.i64", &[x, y]);
106        assert_eq!(selects(&terms, add), Some("x64.add_rr_64"));
107    }
108
109    /// The bindings are the operands in the order the pattern names them, and the replacement
110    /// says which of them goes where. This is the whole of what the selector will read.
111    ///
112    /// What a name is bound to is what the pattern put it under, so `(value.i32 x)` binds the
113    /// register and not the term saying it is one. That is the difference between the operand of
114    /// the instruction this becomes and a wrapper that exists to say how wide it is.
115    #[test]
116    fn a_match_gives_back_the_operands_the_pattern_named() {
117        let mut terms = Terms::default();
118        let first = terms.app("v0", &[]);
119        let second = terms.app("v1", &[]);
120        let x = terms.app("value.i32", &[first]);
121        let y = terms.app("value.i32", &[second]);
122        let sub = terms.app("sub.i32", &[x, y]);
123        let found = TABLE.find(&terms, sub).expect("a rule fires");
124        let rule = TABLE.rule(&found);
125        assert_eq!(rule.pattern, "(sub.i32 (value.i32 x) (value.i32 y))");
126        assert_eq!(found.bindings, vec![first, second]);
127        let names: Vec<&str> = rule
128            .replacement
129            .iter()
130            .filter_map(|piece| match piece {
131                Piece::Var { name, index } => {
132                    assert_eq!(found.bindings[*index], if *index == 0 { first } else { second });
133                    Some(*name)
134                }
135                _ => None,
136            })
137            .collect();
138        assert_eq!(names, ["x", "y"]);
139    }
140
141    /// An immediate the instruction has room for takes the immediate form. The rule for it is
142    /// guarded, so this is also the test that a guard which holds does not stop a rule firing.
143    #[test]
144    fn an_addition_of_an_immediate_that_fits_is_the_immediate_form() {
145        let mut terms = Terms::default();
146        let x = terms.value(64, "v0");
147        let k = terms.constant(4);
148        let k = terms.app("iconst.i64", &[k]);
149        let add = terms.app("add.i64", &[x, k]);
150        assert_eq!(selects(&terms, add), Some("x64.add_ri_64"));
151    }
152
153    /// An immediate too wide for the encoding is what the guard is there to refuse. Nothing else
154    /// matches such a term, and that is the right answer: the constant has to be put in a
155    /// register first, which is a decision for the selector and not for the table.
156    #[test]
157    fn an_addition_of_an_immediate_too_wide_for_the_form_matches_nothing() {
158        let mut terms = Terms::default();
159        let x = terms.value(64, "v0");
160        let k = terms.constant(1 << 40);
161        let k = terms.app("iconst.i64", &[k]);
162        let add = terms.app("add.i64", &[x, k]);
163        assert_eq!(selects(&terms, add), None);
164    }
165
166    /// The other shape of guard, which is a shift count the width allows.
167    #[test]
168    fn a_shift_by_a_count_the_width_allows_is_the_immediate_form() {
169        let mut terms = Terms::default();
170        let x = terms.value(64, "v0");
171        let k = terms.constant(3);
172        let k = terms.app("iconst.i64", &[k]);
173        let shl = terms.app("shl.i64", &[x, k]);
174        assert_eq!(selects(&terms, shl), Some("x64.shl_ri_64"));
175    }
176
177    #[test]
178    fn a_shift_by_a_count_the_width_does_not_allow_matches_nothing() {
179        let mut terms = Terms::default();
180        let x = terms.value(64, "v0");
181        let k = terms.constant(64);
182        let k = terms.app("iconst.i64", &[k]);
183        let shl = terms.app("shl.i64", &[x, k]);
184        assert_eq!(selects(&terms, shl), None);
185    }
186
187    /// One bit reaches the byte instructions, which is the whole of how the machine holds a truth
188    /// value. The widening is the interesting one: it is `movzbl` under a name of its own, so the
189    /// rule that fires here is not the rule a byte would have found.
190    #[test]
191    fn a_truth_value_is_lowered_to_the_byte_instructions_that_keep_it_one() {
192        let mut terms = Terms::default();
193        let x = terms.value(1, "v0");
194        let y = terms.value(1, "v1");
195        let xor = terms.app("xor.i1", &[x, y]);
196        assert_eq!(selects(&terms, xor), Some("x64.xor_rr_8"));
197
198        let x = terms.value(1, "v2");
199        let wide = terms.app("zext.i1.i32", &[x]);
200        assert_eq!(selects(&terms, wide), Some("x64.bit_to_32"));
201
202        let x = terms.value(8, "v3");
203        let byte = terms.app("zext.i8.i32", &[x]);
204        assert_eq!(selects(&terms, byte), Some("x64.movzx_8_32"));
205    }
206
207    /// A term the rule set says nothing about is nothing rather than a wrong answer, which is
208    /// what the completeness check in `spec/10-backend.md` will be for.
209    #[test]
210    fn a_term_no_rule_covers_finds_no_rule() {
211        let mut terms = Terms::default();
212        let x = terms.value(64, "v0");
213        let y = terms.value(64, "v1");
214        let odd = terms.app("no.such.opcode", &[x, y]);
215        assert_eq!(selects(&terms, odd), None);
216    }
217}