Skip to main content

rucc_codegen/select/
aarch64.rs

1//! The AArch64 lowering table.
2//!
3//! Everything below the module comment is generated from `rules/aarch64.rules` by `rucc-rules`
4//! when this crate is built, the same way the x86-64 table is, and `crate::pipeline::Machine`
5//! hands it to the lowering for any target whose architecture is AArch64.
6
7// The guards are emitted as the comparisons the rules write, for the reason the x86-64 table gives.
8#![allow(clippy::manual_range_contains)]
9
10include!(concat!(env!("OUT_DIR"), "/aarch64.rs"));
11
12/// What the lowering asks of AArch64.
13pub static SELECTOR: super::Selector = super::Selector {
14    table: &TABLE,
15    shapes: &rucc_target::aarch64::MACHINE,
16    address: rucc_target::aarch64::address,
17    frame: &rucc_target::aarch64::FRAME,
18    branch: &rucc_target::aarch64::BRANCH,
19    gpr: rucc_target::aarch64::GPR,
20    fence: "fence",
21    trap: "trap",
22    abi: &crate::abi::aarch64::INSTS,
23    scratch: &crate::pipeline::AARCH64_SCRATCH,
24    symbols: &super::Symbols {
25        near: super::Reach::Own("addr_64"),
26        far: super::Reach::Own("got_64"),
27        slot: super::Reach::Own("slot_64"),
28        thread: super::Reach::Own("gottprel_64"),
29        pointer: super::Pointer::Own("thread_64"),
30        indexed: None,
31        teb: Some(super::Teb { index: "ldr_sym_32", array: "teb_64", block: "secrel_64" }),
32    },
33    jumps: &super::Jumps {
34        near: "adr_64",
35        cell: "ldrs_32_64",
36        add: "add_rr_64",
37        two_address: false,
38    },
39};
40
41#[cfg(test)]
42mod tests {
43    use rucc_target::aarch64;
44
45    use super::TABLE;
46    use crate::select::{Piece, Subject};
47
48    /// The prefix the rule file puts in front of a machine term.
49    const PREFIX: &str = "a64.";
50
51    /// The address constructors, which are terms in the rule file and not instructions.
52    const AMODES: &[&str] = &["amode_base", "amode_base_offset"];
53
54    /// A term as a flat arena, which is the shape the x86-64 tests use and the shape the IR has.
55    #[derive(Debug)]
56    enum Node {
57        Int(i128),
58        App(String, Vec<usize>),
59    }
60
61    #[derive(Debug, Default)]
62    struct Terms {
63        nodes: Vec<Node>,
64    }
65
66    impl Terms {
67        fn constant(&mut self, head: &str, value: i128) -> usize {
68            self.nodes.push(Node::Int(value));
69            let at = self.nodes.len() - 1;
70            self.app(head, &[at])
71        }
72
73        fn app(&mut self, head: &str, args: &[usize]) -> usize {
74            self.nodes.push(Node::App(head.to_owned(), args.to_vec()));
75            self.nodes.len() - 1
76        }
77
78        fn value(&mut self, width: &str, name: &str) -> usize {
79            let inner = self.app(name, &[]);
80            self.app(&format!("value.{width}"), &[inner])
81        }
82    }
83
84    impl Subject for Terms {
85        type Node = usize;
86
87        fn head(&self, node: usize) -> Option<(&str, usize)> {
88            match &self.nodes[node] {
89                Node::App(head, args) => Some((head.as_str(), args.len())),
90                Node::Int(_) => None,
91            }
92        }
93
94        fn arg(&self, node: usize, index: usize) -> usize {
95            match &self.nodes[node] {
96                Node::App(_, args) => args[index],
97                Node::Int(_) => unreachable!("a constant has no arguments"),
98            }
99        }
100
101        fn int(&self, node: usize) -> Option<i128> {
102            match self.nodes[node] {
103                Node::Int(value) => Some(value),
104                Node::App(..) => None,
105            }
106        }
107
108        fn same(&self, a: usize, b: usize) -> bool {
109            a == b
110        }
111    }
112
113    fn selects(terms: &Terms, term: usize) -> Option<&'static str> {
114        let found = TABLE.find(terms, term)?;
115        TABLE.rule(&found).head()
116    }
117
118    #[test]
119    fn the_table_holds_every_rule_the_file_writes() {
120        let text = include_str!("../../rules/aarch64.rules");
121        let written = text.lines().filter(|line| line.starts_with("(rule ")).count();
122        assert_eq!(TABLE.rules.len(), written, "the table and the rule file disagree");
123        assert_eq!(TABLE.source, "rules/aarch64.rules");
124    }
125
126    #[test]
127    fn every_instruction_the_table_writes_is_described() {
128        for rule in TABLE.rules {
129            for piece in rule.replacement {
130                let Piece::App { head, .. } = piece else { continue };
131                if AMODES.contains(head) {
132                    continue;
133                }
134                let opcode = head.strip_prefix(PREFIX).unwrap_or_else(|| {
135                    panic!("line {}: {head} is neither an AArch64 term nor an address", rule.line)
136                });
137                assert!(
138                    aarch64::form(opcode).is_some(),
139                    "line {}: {head} is selected and `rucc_target::aarch64` does not describe it",
140                    rule.line
141                );
142            }
143        }
144    }
145
146    /// The narrow widths take the thirty two bit instruction for the operations whose low bits do
147    /// not depend on the high ones, and nothing else reaches them.
148    #[test]
149    fn narrow_arithmetic_is_the_thirty_two_bit_instruction() {
150        let mut terms = Terms::default();
151        for width in ["i8", "i16", "i32"] {
152            let x = terms.value(width, "v0");
153            let y = terms.value(width, "v1");
154            let add = terms.app(&format!("add.{width}"), &[x, y]);
155            assert_eq!(selects(&terms, add), Some("a64.add_rr_32"));
156            let mul = terms.app(&format!("mul.{width}"), &[x, y]);
157            assert_eq!(selects(&terms, mul), Some("a64.mul_rr_32"));
158        }
159        let x = terms.value("i8", "v0");
160        let y = terms.value("i8", "v1");
161        let shift = terms.app("lshr.i8", &[x, y]);
162        assert_eq!(selects(&terms, shift), None);
163        let x = terms.value("i64", "v2");
164        let y = terms.value("i64", "v3");
165        let add = terms.app("add.i64", &[x, y]);
166        assert_eq!(selects(&terms, add), Some("a64.add_rr_64"));
167    }
168
169    /// A narrow right shift by a constant is the instruction that widens first, since the bits
170    /// above a byte or a half in its register are not known.
171    #[test]
172    fn a_narrow_right_shift_by_a_constant_is_the_instruction_that_widens_first() {
173        let mut terms = Terms::default();
174        for (width, bits) in [("i8", 7), ("i16", 15)] {
175            let x = terms.value(width, "v0");
176            let k = terms.constant(&format!("iconst.{width}"), 3);
177            let right = terms.app(&format!("lshr.{width}"), &[x, k]);
178            assert_eq!(selects(&terms, right), Some(&*format!("a64.lsr_ri_{}", &width[1..])));
179            let right = terms.app(&format!("ashr.{width}"), &[x, k]);
180            assert_eq!(selects(&terms, right), Some(&*format!("a64.asr_ri_{}", &width[1..])));
181            let far = terms.constant(&format!("iconst.{width}"), bits + 1);
182            let right = terms.app(&format!("lshr.{width}"), &[x, far]);
183            assert_eq!(selects(&terms, right), None);
184        }
185    }
186
187    /// A constant is one `mov` when it or its complement fits in sixteen bits, or when every
188    /// sixteen bit piece but one is zero or every one but one is all ones. Anything else is built a
189    /// piece at a time, and the outermost instruction says how many pieces: one `movk` under two
190    /// to the thirty two, two when the top piece is zero, and three otherwise.
191    #[test]
192    fn a_constant_is_one_mov_only_when_one_mov_can_build_it() {
193        let mut terms = Terms::default();
194        let wanted = [
195            (0, "a64.mov_ri_64"),
196            (65535, "a64.mov_ri_64"),
197            (-65536, "a64.mov_ri_64"),
198            (65536, "a64.mov_ri_64"),
199            (0x1_0000_0000, "a64.mov_ri_64"),
200            (0x4008_0000_0000_0000, "a64.mov_ri_64"),
201            (-0x1234_0000_0001, "a64.mov_ri_64"),
202            (-0x1_0001, "a64.mov_ri_64"),
203            (0x1_0001, "a64.movk_ri_16_64"),
204            (0xffff_ffff, "a64.movk_ri_16_64"),
205            (0x1_2345_6789, "a64.movk_ri_32_64"),
206            (0x1_0000_0001, "a64.movk_ri_32_64"),
207            (-0x1_0002, "a64.movk_ri_48_64"),
208            (0x1_0000_0000_0001, "a64.movk_ri_48_64"),
209        ];
210        for (value, want) in wanted {
211            let k = terms.constant("iconst.i64", value);
212            assert_eq!(selects(&terms, k), Some(want), "{value}");
213        }
214        let wanted = [
215            (0x1234_5678, "a64.movk_ri_16_32"),
216            (0x4040_0000, "a64.mov_ri_32"),
217            (0x1234_ffff, "a64.mov_ri_32"),
218            (i128::from(i32::MIN), "a64.mov_ri_32"),
219        ];
220        for (value, want) in wanted {
221            let k = terms.constant("iconst.i32", value);
222            assert_eq!(selects(&terms, k), Some(want), "{value}");
223        }
224    }
225
226    /// Every widening and narrowing the IR has between its integer types has a rule.
227    #[test]
228    fn every_widening_and_narrowing_has_an_instruction() {
229        let mut terms = Terms::default();
230        let wanted = [
231            ("sext", "i8", "i16", "a64.sxtb_16"),
232            ("zext", "i8", "i16", "a64.uxtb_16"),
233            ("zext", "i8", "i64", "a64.uxtb_64"),
234            ("zext", "i16", "i64", "a64.uxth_64"),
235            ("zext", "i1", "i8", "a64.bit_to_8"),
236            ("zext", "i1", "i64", "a64.bit_to_64"),
237            ("trunc", "i64", "i32", "a64.low_32"),
238            ("trunc", "i32", "i16", "a64.low_16"),
239            ("trunc", "i16", "i8", "a64.low_8"),
240            ("trunc", "i8", "i1", "a64.bit_of_32"),
241            ("trunc", "i64", "i1", "a64.bit_of_64"),
242        ];
243        for (op, from, to, want) in wanted {
244            let x = terms.value(from, "v0");
245            let term = terms.app(&format!("{op}.{from}.{to}"), &[x]);
246            assert_eq!(selects(&terms, term), Some(want), "{op}.{from}.{to}");
247        }
248    }
249
250    #[test]
251    fn an_immediate_is_taken_when_twelve_bits_hold_it() {
252        let mut terms = Terms::default();
253        let x = terms.value("i32", "v0");
254        let k = terms.constant("iconst.i32", 4095);
255        let add = terms.app("add.i32", &[x, k]);
256        assert_eq!(selects(&terms, add), Some("a64.add_ri_32"));
257        let k = terms.constant("iconst.i32", 4096);
258        let add = terms.app("add.i32", &[x, k]);
259        assert_eq!(selects(&terms, add), None);
260    }
261
262    /// The IR's unsigned predicates are under the architecture's names for them.
263    #[test]
264    fn an_unsigned_comparison_is_the_condition_the_architecture_names() {
265        let mut terms = Terms::default();
266        let x = terms.value("i64", "v0");
267        let y = terms.value("i64", "v1");
268        for (ir, cc) in [("ult", "lo"), ("ule", "ls"), ("ugt", "hi"), ("uge", "hs")] {
269            let term = terms.app(&format!("icmp_{ir}.i1"), &[x, y]);
270            let want = format!("a64.cmp_set_{cc}_64");
271            assert_eq!(selects(&terms, term), Some(want.as_str()));
272        }
273    }
274
275    /// Every comparison against a register has one against a constant, for the reason the x86-64
276    /// table counts them.
277    #[test]
278    fn a_comparison_against_a_constant_is_written_for_every_one_against_a_register() {
279        let mut against_register = Vec::new();
280        let mut against_constant = Vec::new();
281        let mut narrow = 0;
282        for rule in TABLE.rules {
283            let Some(rest) = rule.pattern.strip_prefix("(icmp_") else { continue };
284            let (condition, operands) = rest.split_once(".i1 ").expect("a comparison takes two");
285            let width = operands
286                .strip_prefix("(value.")
287                .and_then(|rest| rest.split_once(' '))
288                .map(|(width, _)| width)
289                .expect("a comparison reads a value first");
290            // The narrow widths compare two registers, each widened first, and have no form
291            // against a constant.
292            if matches!(width, "i8" | "i16") {
293                narrow += 1;
294                continue;
295            }
296            let named = format!("{condition}.{width}");
297            if operands.contains("(iconst.") {
298                against_constant.push(named);
299            } else {
300                against_register.push(named);
301            }
302        }
303        against_register.sort_unstable();
304        against_constant.sort_unstable();
305        assert_eq!(against_register, against_constant);
306        assert_eq!(against_register.len(), 20, "ten conditions at two widths");
307        assert_eq!(narrow, 20, "ten conditions at the two narrow widths");
308    }
309
310    /// The value comes first in a store, which is where the IR keeps it.
311    #[test]
312    fn a_store_is_written_with_the_value_first() {
313        let mut seen = 0;
314        for rule in TABLE.rules {
315            let Some(rest) = rule.pattern.strip_prefix("(store.") else { continue };
316            let (width, operands) = rest.split_once(' ').expect("a store takes operands");
317            assert!(
318                operands.starts_with(&format!("(value.{width} ")),
319                "line {}: {} binds something other than the value first",
320                rule.line,
321                rule.pattern
322            );
323            seen += 1;
324        }
325        assert_eq!(seen, 16, "the store rules moved and this test did not follow them");
326    }
327
328    /// An offset the unscaled form holds goes into the address, and one it does not leaves the
329    /// addition where it is.
330    #[test]
331    fn a_small_offset_is_part_of_the_address() {
332        let mut terms = Terms::default();
333        let a = terms.value("i64", "v0");
334        let k = terms.constant("iconst.i64", -8);
335        let at = terms.app("add.i64", &[a, k]);
336        let load = terms.app("load.i32", &[at]);
337        let found = TABLE.find(&terms, load).expect("a rule fires");
338        assert_eq!(TABLE.rule(&found).head(), Some("a64.ldr_32"));
339        assert!(
340            TABLE
341                .rule(&found)
342                .replacement
343                .iter()
344                .any(|piece| matches!(piece, Piece::App { head: "amode_base_offset", .. }))
345        );
346        let k = terms.constant("iconst.i64", 256);
347        let at = terms.app("add.i64", &[a, k]);
348        let load = terms.app("load.i32", &[at]);
349        assert_eq!(selects(&terms, load), None);
350    }
351}