Skip to main content

rucc_ir/
asm.rs

1//! Reading an assembly statement's operands back off the instruction.
2//!
3//! Design: `spec/11-asm-objects-debug.md` section 11.2, which owns what a constraint means.
4//!
5//! [`crate::AsmInfo`] carries one comma separated constraint list, in the order the template
6//! numbers its operands, which is the outputs and then the inputs. The values are somewhere else:
7//! an output travelling in a register is a result of the instruction and everything else is an
8//! operand of it. Nothing records which entry took which, because the list already says it, and a
9//! second copy of the answer would be a second thing to keep in step with the first.
10//!
11//! [`AsmOperands::read`] is that answer worked out once, so that the back end and anything else
12//! that wants it are reading the same rule rather than each writing the scan out again.
13//!
14//! # Why it can fail
15//!
16//! Which file an operand travels in is the front end's decision and the front end knows the type,
17//! which this does not. A structure is handed over as an address whatever its constraint says, and
18//! there is nothing in `"=r"` that says so. So the scan works out what the constraint alone implies
19//! and then counts what it worked out against the results and the operands the instruction actually
20//! has. A disagreement means the constraint is not the whole story for that statement, and the
21//! answer is nothing at all rather than a guess, because the caller's next move is to place values
22//! and placing them by a guess is a wrong program rather than a refused one.
23
24use crate::Value;
25
26/// What one entry of a constraint list is for.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum AsmRole {
29    /// An output, written `=` or `+`, which the assembly writes.
30    Output,
31    /// An input, which it reads.
32    Input,
33}
34
35/// One operand of an assembly statement, as its constraint describes it.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct AsmOperand {
38    /// Which side of the colon it was written on.
39    pub role: AsmRole,
40    /// Whether the assembly is handed the address of an object rather than a value.
41    pub memory: bool,
42    /// The result it produces, for an output that travels in a register.
43    pub result: Option<Value>,
44    /// The value it reads, which every entry but a register output written `=` has.
45    ///
46    /// An output written `+` is read as well as written, so it has both this and a result. An
47    /// output in memory has this and no result, because what the assembly was handed is the address
48    /// and the address is an operand like any other.
49    pub value: Option<Value>,
50    /// The output an input written as a number shares its place with.
51    ///
52    /// `"0"` is the whole of what a matching constraint says: this input and that output are one
53    /// place, so whatever the assembly leaves there is what the output gets.
54    pub tied: Option<usize>,
55}
56
57/// The operands of one assembly statement, in the order the template counts them.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct AsmOperands {
60    list: Vec<AsmOperand>,
61}
62
63impl AsmOperands {
64    /// The operands of a statement with that constraint list, those results and those values.
65    ///
66    /// Nothing when the list does not describe what the instruction carries, which is what a
67    /// constraint that is not the whole story looks like from here. See the module documentation.
68    #[must_use]
69    pub fn read(constraints: &str, results: &[Value], values: &[Value]) -> Option<AsmOperands> {
70        // An empty list is no operands and not one operand spelled with nothing, which is what
71        // splitting the empty string on commas would otherwise give.
72        let written: Vec<&str> =
73            if constraints.is_empty() { Vec::new() } else { constraints.split(',').collect() };
74
75        let mut list = Vec::with_capacity(written.len());
76        let mut result = results.iter();
77        let mut value = values.iter();
78        for text in written {
79            let entry = Entry::read(text)?;
80            // An output in a register is the one entry that takes a result, and it takes a value as
81            // well when it was written `+`, because that is an output the assembly reads first.
82            let register = entry.role == AsmRole::Output && !entry.memory;
83            let taken = if register { Some(result.next().copied()?) } else { None };
84            let read = if register && !entry.updates { None } else { Some(value.next().copied()?) };
85            list.push(AsmOperand {
86                role: entry.role,
87                memory: entry.memory,
88                result: taken,
89                value: read,
90                tied: entry.tied,
91            });
92        }
93
94        // Every result and every value accounted for. One left over means the list describes fewer
95        // operands than the statement has, which is the disagreement this is looking for.
96        if result.next().is_some() || value.next().is_some() {
97            return None;
98        }
99        // A number naming an output that is not there, or naming one that has no result to share,
100        // is a list that cannot be placed however the counts came out.
101        for entry in &list {
102            if let Some(tied) = entry.tied {
103                if !list.get(tied).is_some_and(|output| output.result.is_some()) {
104                    return None;
105                }
106            }
107        }
108        Some(AsmOperands { list })
109    }
110
111    /// The operands, in the order the template counts them.
112    pub fn iter(&self) -> impl Iterator<Item = &AsmOperand> {
113        self.list.iter()
114    }
115
116    /// The value an output shares its place with, which is what a matching constraint asks for.
117    ///
118    /// Written `+` on the output itself, or written as that output's number on an input, and the
119    /// two mean the same thing to whatever has to place the values.
120    #[must_use]
121    pub fn tied_to(&self, output: usize) -> Option<Value> {
122        let written = self.list.get(output)?;
123        if written.value.is_some() {
124            return written.value;
125        }
126        self.list.iter().find(|entry| entry.tied == Some(output)).and_then(|entry| entry.value)
127    }
128
129    /// How many operands there are.
130    #[must_use]
131    pub fn len(&self) -> usize {
132        self.list.len()
133    }
134
135    /// Whether there are none, which is what an `asm` with a bare template has.
136    #[must_use]
137    pub fn is_empty(&self) -> bool {
138        self.list.is_empty()
139    }
140}
141
142/// One constraint, read for the three things the scan above needs from it.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144struct Entry {
145    role: AsmRole,
146    memory: bool,
147    updates: bool,
148    tied: Option<usize>,
149}
150
151impl Entry {
152    /// One constraint, or nothing for one with a letter this does not know.
153    ///
154    /// Not knowing a letter is the same answer as a count that does not add up and for the same
155    /// reason. A letter nobody has read is a letter that may mean the operand is somewhere other
156    /// than where the rest of the constraint suggests.
157    fn read(text: &str) -> Option<Entry> {
158        let mut role = AsmRole::Input;
159        let mut updates = false;
160        let mut memory = false;
161        let mut register = false;
162        let mut tied = None;
163
164        let mut rest = text.chars().peekable();
165        while let Some(letter) = rest.next() {
166            match letter {
167                '=' => role = AsmRole::Output,
168                '+' => {
169                    role = AsmRole::Output;
170                    updates = true;
171                }
172                // Earlyclobber and commutative, which say when the operand is written and whether
173                // it may swap with the one after it. Neither changes where it lives, and where it
174                // lives is the whole of what this reads.
175                '&' | '%' => {}
176                // The memory forms. `o` and `V` are the offsettable and the non offsettable halves
177                // of `m`, and all three are an address as far as anything here is concerned.
178                'm' | 'o' | 'V' => memory = true,
179                // A register, an immediate, or either. `g` and `X` allow memory as well, and the
180                // front end takes the register when it has the choice, so they count as registers
181                // here for the same reason `rm` does.
182                'r' | 'g' | 'X' | 'i' | 'n' | 's' | 'a' | 'b' | 'c' | 'd' | 'S' | 'D' | 'A'
183                | 'q' | 'Q' | 'f' | 't' | 'u' | 'x' | 'y' | 'v' | 'l' | 'e' | 'k' | 'h' | 'j'
184                | 'z' | 'w' => register = true,
185                // The immediate ranges, which are `I` through `P` on x86 and are a constant
186                // wherever they are read.
187                'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' => {
188                    register = true;
189                }
190                // A matching constraint, which is the number of the output this shares a place
191                // with. More than one digit is a statement with more than ten operands, and the
192                // number is the whole run of them rather than the first.
193                '0'..='9' => {
194                    let mut number = letter.to_digit(10)? as usize;
195                    while let Some(next) = rest.peek().and_then(|&c| c.to_digit(10)) {
196                        number = number * 10 + next as usize;
197                        rest.next();
198                    }
199                    tied = Some(number);
200                    register = true;
201                }
202                _ => return None,
203            }
204        }
205
206        // A constraint that named nothing at all is not one the front end would have produced, and
207        // reading it as a register would be reading it as something it does not say.
208        if !memory && !register {
209            return None;
210        }
211        Some(Entry { role, memory: memory && !register, updates, tied })
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::Value;
219
220    /// Values numbered from zero, which is all the scan looks at.
221    fn values(count: usize) -> Vec<Value> {
222        (0..count).map(Value::from_usize).collect()
223    }
224
225    #[test]
226    fn a_statement_with_no_operands_has_none() {
227        let read =
228            AsmOperands::read("", &[], &[]).expect("an empty list describes an empty statement");
229        assert!(read.is_empty());
230    }
231
232    #[test]
233    fn an_input_in_a_register_reads_a_value_and_produces_nothing() {
234        let values = values(1);
235        let read = AsmOperands::read("r", &[], &values).expect("one input");
236        let [operand] = read.iter().copied().collect::<Vec<_>>()[..] else { panic!("one operand") };
237        assert_eq!(operand.role, AsmRole::Input);
238        assert!(!operand.memory);
239        assert_eq!(operand.result, None);
240        assert_eq!(operand.value, Some(values[0]));
241    }
242
243    #[test]
244    fn an_output_in_a_register_produces_a_result_and_reads_nothing() {
245        let results = values(1);
246        let read = AsmOperands::read("=r", &results, &[]).expect("one output");
247        let operand = read.iter().next().copied().expect("one operand");
248        assert_eq!(operand.role, AsmRole::Output);
249        assert_eq!(operand.result, Some(results[0]));
250        assert_eq!(operand.value, None);
251        assert_eq!(read.tied_to(0), None);
252    }
253
254    #[test]
255    fn an_output_written_plus_reads_the_value_it_overwrites() {
256        let results = values(1);
257        let args = values(1);
258        let read = AsmOperands::read("+r", &results, &args).expect("one output read and written");
259        let operand = read.iter().next().copied().expect("one operand");
260        assert_eq!(operand.result, Some(results[0]));
261        assert_eq!(operand.value, Some(args[0]));
262        assert_eq!(read.tied_to(0), Some(args[0]));
263    }
264
265    #[test]
266    fn an_input_written_as_a_number_shares_the_place_of_that_output() {
267        let results = values(1);
268        let args = values(1);
269        let read = AsmOperands::read("=r,0", &results, &args).expect("an output and its match");
270        let list: Vec<AsmOperand> = read.iter().copied().collect();
271        assert_eq!(list[1].tied, Some(0));
272        assert_eq!(read.tied_to(0), Some(args[0]));
273    }
274
275    #[test]
276    fn an_operand_in_memory_is_an_address_whichever_side_it_is_on() {
277        let args = values(2);
278        let read = AsmOperands::read("=m,m", &[], &args).expect("an output and an input in memory");
279        let list: Vec<AsmOperand> = read.iter().copied().collect();
280        assert!(list[0].memory && list[1].memory);
281        assert_eq!(list[0].result, None);
282        assert_eq!(list[0].value, Some(args[0]));
283        assert_eq!(list[1].value, Some(args[1]));
284    }
285
286    #[test]
287    fn a_constraint_that_allows_a_register_or_memory_takes_the_register() {
288        let results = values(1);
289        let read = AsmOperands::read("=rm", &results, &[]).expect("an output that could be either");
290        let operand = read.iter().next().copied().expect("one operand");
291        assert!(!operand.memory);
292        assert_eq!(operand.result, Some(results[0]));
293    }
294
295    #[test]
296    fn a_list_describing_more_results_than_there_are_is_refused() {
297        assert_eq!(AsmOperands::read("=r,=r", &values(1), &[]), None);
298    }
299
300    #[test]
301    fn a_list_describing_fewer_values_than_there_are_is_refused() {
302        assert_eq!(AsmOperands::read("r", &[], &values(2)), None);
303    }
304
305    #[test]
306    fn a_number_naming_an_output_that_is_not_there_is_refused() {
307        assert_eq!(AsmOperands::read("=r,3", &values(1), &values(1)), None);
308    }
309
310    #[test]
311    fn a_number_naming_an_output_in_memory_is_refused() {
312        assert_eq!(AsmOperands::read("=m,0", &[], &values(2)), None);
313    }
314
315    #[test]
316    fn a_letter_nobody_has_read_is_refused() {
317        assert_eq!(AsmOperands::read("^", &[], &values(1)), None);
318    }
319
320    #[test]
321    fn a_constraint_that_names_nowhere_at_all_is_refused() {
322        assert_eq!(AsmOperands::read("&", &[], &values(1)), None);
323    }
324
325    #[test]
326    fn a_number_of_more_than_one_digit_is_the_whole_run() {
327        let results = values(11);
328        let args = values(1);
329        let outputs = ["=r"; 11].join(",");
330        let read = AsmOperands::read(&format!("{outputs},10"), &results, &args)
331            .expect("eleven outputs and a match on the last");
332        let list: Vec<AsmOperand> = read.iter().copied().collect();
333        assert_eq!(list[11].tied, Some(10));
334        assert_eq!(read.tied_to(10), Some(args[0]));
335    }
336}