Skip to main content

rucc_verify/
model.rs

1//! What the terms in a rule mean, in bitvectors.
2//!
3//! A rule relates an IR term to a machine term and claims the two compute the same thing. A
4//! solver cannot check that claim without being told what the terms are, so every head a rule
5//! uses needs an entry here. `spec/10-backend.md` calls this Crocus's stated tax and says to pay
6//! it from the first rule rather than retrofitting it, which is why a head with no entry is an
7//! error rather than an unchecked assumption.
8//!
9//! The model is written in the same language as the rules:
10//!
11//! ```text
12//! (semantics (amode_base_index_scale base index scale) (bvadd base (bvmul index scale)))
13//! (semantics (x64.lea address) address)
14//! ```
15//!
16//! Anything the solver already knows is not written down. Those are the [`BUILTIN`] heads, and
17//! they are spelled the way SMT-LIB spells them except for the comparisons, where a rule writes
18//! `<` and the solver wants `bvslt`.
19//!
20//! # Widths
21//!
22//! Every term is some number of bits wide and [`Widths`] is what says how many. A head that ends
23//! in `.iN` is N bits wide, anything else is as wide as the term it sits inside, and a name is as
24//! wide as the place in the pattern that bound it. That is enough for a rule to convert between
25//! widths, which is what `sext`, `zext` and `trunc` all are, and those conversions are written
26//! the way `spec/10-backend.md` writes them: `(sign_extend 32 64 x)` and `(extract 31 0 x)`, with
27//! the widths spelled out rather than left to be inferred.
28//!
29//! The widths are checked here rather than left to the solver, because a solver handed two
30//! bitvectors of different sorts says so in its own words and at a place in generated text that
31//! nobody wants to read.
32
33use std::collections::{BTreeMap, HashMap};
34
35use rucc_rules::{Error, Term, TermKind, parse_terms};
36
37/// The heads the solver already understands, and what SMT-LIB calls them.
38///
39/// The comparisons written as symbols are the signed ones. An unsigned comparison in a rule has
40/// to be written with the solver's own name for it, which is deliberate: a rule that means the
41/// unsigned one should have to say so rather than depend on which way this table happens to read.
42/// Both families are here under those names as well, so a rule that would rather be explicit
43/// about the signed one can be.
44const BUILTIN: [(&str, &str); 32] = [
45    ("=", "="),
46    ("and", "and"),
47    ("or", "or"),
48    ("not", "not"),
49    ("<", "bvslt"),
50    ("<=", "bvsle"),
51    (">", "bvsgt"),
52    (">=", "bvsge"),
53    ("bvslt", "bvslt"),
54    ("bvsle", "bvsle"),
55    ("bvsgt", "bvsgt"),
56    ("bvsge", "bvsge"),
57    ("bvult", "bvult"),
58    ("bvule", "bvule"),
59    ("bvugt", "bvugt"),
60    ("bvuge", "bvuge"),
61    ("bvadd", "bvadd"),
62    ("bvsub", "bvsub"),
63    ("bvmul", "bvmul"),
64    ("bvneg", "bvneg"),
65    ("bvnot", "bvnot"),
66    ("bvand", "bvand"),
67    ("bvor", "bvor"),
68    ("bvxor", "bvxor"),
69    ("bvshl", "bvshl"),
70    ("bvlshr", "bvlshr"),
71    ("bvashr", "bvashr"),
72    ("bvsdiv", "bvsdiv"),
73    ("bvudiv", "bvudiv"),
74    ("bvsrem", "bvsrem"),
75    ("bvurem", "bvurem"),
76    ("ite", "ite"),
77];
78
79/// The builtins that take a boolean somewhere, so their arguments are not all one width and
80/// there is nothing to check between them.
81const LOGICAL: [&str; 4] = ["and", "or", "not", "ite"];
82
83/// The heads that change width. Their first two arguments are widths rather than values, which
84/// is why they are written out here rather than sitting in [`BUILTIN`] with the rest: SMT-LIB
85/// spells them as indexed operators and the index is a number this has to work out.
86const CONVERSION: [&str; 3] = ["sign_extend", "zero_extend", "extract"];
87
88/// What a rule works in when its opcode does not say. Every opcode in the IR does say, so this
89/// is what a hand written test rule gets rather than something the real rule set relies on.
90pub const DEFAULT_WIDTH: u32 = 64;
91
92/// How wide each thing in one rule is.
93///
94/// A rule is written at one width, the one its pattern's opcode names, and the terms inside it
95/// may say another: `(value.i64 x)` under an `add.i32` is a thirty two bit add of two sixty four
96/// bit registers, which is the shape every `sext`, `zext` and `trunc` in a lowering has. What a
97/// name stands at is fixed by the pattern, because the pattern is where a name is bound, and
98/// everywhere else reads it from here.
99///
100/// A bounded proof asks the same rule at a narrower width, and that scales every width in the
101/// rule by one ratio rather than flattening them all to one number. A rule that converts between
102/// widths still converts between widths when it is asked at eight bits, which it would not do if
103/// the narrow width were simply substituted everywhere.
104#[derive(Debug, Clone, Default)]
105pub struct Widths {
106    /// The width the rule is written in.
107    natural: u32,
108    /// The width it is being asked at, which is the same number unless this is a bounded proof.
109    asked: u32,
110    /// What each name the pattern binds stands at, already scaled.
111    at: BTreeMap<String, u32>,
112}
113
114impl Widths {
115    /// The widths one rule's pattern fixes, at the width the rule is written in.
116    #[must_use]
117    pub fn of(pattern: &Term) -> Widths {
118        Widths::at(pattern, rule_width(pattern))
119    }
120
121    /// The same, scaled to a width somebody asked for. This is what a bounded proof is made of.
122    #[must_use]
123    pub fn at(pattern: &Term, asked: u32) -> Widths {
124        let natural = rule_width(pattern);
125        let mut widths = Widths { natural, asked, at: BTreeMap::new() };
126        widths.bind(pattern, asked);
127        widths
128    }
129
130    /// The width a term is at when nothing inside it says otherwise.
131    #[must_use]
132    pub fn width(&self) -> u32 {
133        self.asked
134    }
135
136    /// The width the rule is written in, which is the one it will run at.
137    #[must_use]
138    pub fn natural(&self) -> u32 {
139        self.natural
140    }
141
142    /// Every name the pattern binds and how wide it is, sorted.
143    ///
144    /// Sorted rather than in the order the pattern binds them, because the query is something a
145    /// test pins and a diff is easier to read than it is to regenerate.
146    pub fn names(&self) -> impl Iterator<Item = (&str, u32)> {
147        self.at.iter().map(|(name, width)| (name.as_str(), *width))
148    }
149
150    /// These widths and one more name, which is how the replacement's own meaning gets a width
151    /// once it has been substituted into the specification for `(result)`.
152    #[must_use]
153    pub fn with(&self, name: &str, width: u32) -> Widths {
154        let mut out = self.clone();
155        out.at.insert(name.to_owned(), width);
156        out
157    }
158
159    /// How wide a name is, when the pattern bound it.
160    fn of_name(&self, name: &str) -> Option<u32> {
161        self.at.get(name).copied()
162    }
163
164    /// The width a head names, scaled.
165    fn suffix(&self, head: &str) -> Option<u32> {
166        declared(head).map(|width| self.scale(width))
167    }
168
169    /// A width, in the proportion the question is being asked at. Never nothing: a width that
170    /// scales to zero bits is a width the rule cannot be asked about at all.
171    fn scale(&self, width: u32) -> u32 {
172        if self.asked == self.natural || self.natural == 0 {
173            return width;
174        }
175        self.index(width).max(1)
176    }
177
178    /// A bit position, in the same proportion. Zero stays zero, which is what separates this
179    /// from [`Widths::scale`].
180    fn index(&self, position: u32) -> u32 {
181        if self.asked == self.natural || self.natural == 0 {
182            return position;
183        }
184        let scaled = u64::from(position) * u64::from(self.asked) / u64::from(self.natural);
185        u32::try_from(scaled).unwrap_or(position)
186    }
187
188    /// Walk the pattern and write down what each name it binds stands at.
189    fn bind(&mut self, term: &Term, context: u32) {
190        match &term.kind {
191            TermKind::Var(name) => {
192                self.at.insert(name.clone(), context);
193            }
194            TermKind::Int(_) => {}
195            TermKind::App { head, args } => {
196                let inner = self.suffix(head).unwrap_or(context);
197                for arg in args {
198                    self.bind(arg, inner);
199                }
200            }
201        }
202    }
203}
204
205/// The width a rule works in, taken from the suffix on its pattern's opcode.
206#[must_use]
207pub fn rule_width(pattern: &Term) -> u32 {
208    match &pattern.kind {
209        TermKind::App { head, .. } => declared(head).unwrap_or(DEFAULT_WIDTH),
210        _ => DEFAULT_WIDTH,
211    }
212}
213
214/// The width a head names, if it names one. `add.i32` does and `x64.lea` does not.
215fn declared(head: &str) -> Option<u32> {
216    head.rsplit_once('.')
217        .and_then(|(_, suffix)| suffix.strip_prefix('i'))
218        .and_then(|bits| bits.parse::<u32>().ok())
219}
220
221/// What one head means.
222#[derive(Debug, Clone)]
223struct Meaning {
224    /// The names the body is written in terms of.
225    params: Vec<String>,
226    /// What it computes.
227    body: Term,
228}
229
230/// Everything the rules are allowed to say, and what each of it means.
231#[derive(Debug, Default)]
232pub struct Model {
233    heads: HashMap<String, Meaning>,
234}
235
236impl Model {
237    /// Read a model from text.
238    ///
239    /// # Errors
240    ///
241    /// Anything that is not a well formed `(semantics (head params) body)` form, and any head
242    /// given a meaning twice.
243    pub fn read(path: &str, text: &str) -> Result<Model, Vec<Error>> {
244        let terms = parse_terms(path, text)?;
245        let mut model = Model::default();
246        let mut errors = Vec::new();
247
248        for term in terms {
249            let TermKind::App { head, args } = &term.kind else {
250                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
251                continue;
252            };
253            if head != "semantics" || args.len() != 2 {
254                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
255                continue;
256            }
257            let TermKind::App { head: name, args: params } = &args[0].kind else {
258                errors.push(fail(path, &args[0], "expected a head and its parameters".to_owned()));
259                continue;
260            };
261            let mut names = Vec::new();
262            for param in params {
263                match &param.kind {
264                    TermKind::Var(name) => names.push(name.clone()),
265                    _ => errors.push(fail(path, param, "a parameter has to be a name".to_owned())),
266                }
267            }
268            if known(name) {
269                let said = format!("`{name}` is something the solver already knows");
270                errors.push(fail(path, &args[0], said));
271                continue;
272            }
273            let meaning = Meaning { params: names, body: args[1].clone() };
274            if model.heads.insert(name.clone(), meaning).is_some() {
275                let said = format!("`{name}` is given a meaning twice");
276                errors.push(fail(path, &args[0], said));
277            }
278        }
279
280        if errors.is_empty() { Ok(model) } else { Err(errors) }
281    }
282
283    /// Write one term out as SMT-LIB, expanding everything the model defines, and say how wide
284    /// what it computes is.
285    ///
286    /// # Errors
287    ///
288    /// A head that is neither a builtin nor in the model, since that is a term nobody has said
289    /// the meaning of, an application of the wrong number of arguments, and anything whose
290    /// widths do not fit together.
291    pub fn write(&self, path: &str, term: &Term, widths: &Widths) -> Result<(String, u32), Error> {
292        self.write_at(path, term, widths.width(), widths, &HashMap::new())
293    }
294
295    fn write_at(
296        &self,
297        path: &str,
298        term: &Term,
299        context: u32,
300        widths: &Widths,
301        bound: &HashMap<&str, (String, u32)>,
302    ) -> Result<(String, u32), Error> {
303        match &term.kind {
304            TermKind::Var(name) => match bound.get(name.as_str()) {
305                Some((already, width)) => Ok((already.clone(), *width)),
306                None => Ok((name.clone(), widths.of_name(name).unwrap_or(context))),
307            },
308            TermKind::Int(value) => Ok((literal(*value, context), context)),
309            TermKind::App { head, args } => {
310                if CONVERSION.contains(&head.as_str()) {
311                    return self.convert(path, term, head, args, context, widths, bound);
312                }
313                if let Some(name) = builtin(head) {
314                    return self.combine(path, term, head, name, args, context, widths, bound);
315                }
316                let own = widths.suffix(head).unwrap_or(context);
317                let mut written = Vec::with_capacity(args.len());
318                for arg in args {
319                    written.push(self.write_at(path, arg, own, widths, bound)?);
320                }
321                let Some(meaning) = self.heads.get(head) else {
322                    let said = format!("nothing in the model says what `{head}` means");
323                    return Err(fail(path, term, said));
324                };
325                if meaning.params.len() != written.len() {
326                    let said = format!(
327                        "`{head}` means something with {} arguments and this gives it {}",
328                        meaning.params.len(),
329                        written.len()
330                    );
331                    return Err(fail(path, term, said));
332                }
333                let inner: HashMap<&str, (String, u32)> =
334                    meaning.params.iter().map(String::as_str).zip(written).collect();
335                let (text, width) = self.write_at(path, &meaning.body, own, widths, &inner)?;
336                // An opcode that names a width has to mean something that wide. This is the
337                // model being held to what the rules say about it: `add.i32` over registers
338                // that are sixty four bits wide means an add of their low halves, and a model
339                // that leaves the truncation out says so here rather than in a proof that
340                // quietly asks the wrong question.
341                if let Some(said) = widths.suffix(head) {
342                    if said != width {
343                        let told = format!(
344                            "`{head}` is written for {said} bits and means something {width} \
345                             bits wide"
346                        );
347                        return Err(fail(path, term, told));
348                    }
349                }
350                Ok((text, width))
351            }
352        }
353    }
354
355    /// One of the heads the solver already knows, applied to arguments that all have to be the
356    /// same width unless a boolean is involved.
357    #[allow(clippy::too_many_arguments)]
358    fn combine(
359        &self,
360        path: &str,
361        term: &Term,
362        head: &str,
363        name: &str,
364        args: &[Term],
365        context: u32,
366        widths: &Widths,
367        bound: &HashMap<&str, (String, u32)>,
368    ) -> Result<(String, u32), Error> {
369        let mut written = Vec::with_capacity(args.len());
370        for arg in args {
371            written.push(self.write_at(path, arg, context, widths, bound)?);
372        }
373        let Some((_, first)) = written.first() else {
374            return Err(fail(path, term, format!("`{head}` needs arguments")));
375        };
376        let first = *first;
377        if !LOGICAL.contains(&head) {
378            if let Some((_, other)) = written.iter().find(|(_, width)| *width != first) {
379                let said = format!(
380                    "`{head}` is given something {first} bits wide and something {other} bits \
381                     wide, and those are not the same kind of thing"
382                );
383                return Err(fail(path, term, said));
384            }
385        }
386        // A comparison computes a boolean and its width is nobody's business, so saying it is
387        // as wide as what it compared costs nothing and keeps every term having an answer.
388        let width = if head == "ite" && written.len() > 1 { written[1].1 } else { first };
389        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
390        Ok((format!("({name} {})", texts.join(" ")), width))
391    }
392
393    /// A conversion between widths, written as `spec/10-backend.md` writes it, with the widths
394    /// as arguments rather than inferred from anything.
395    #[allow(clippy::too_many_arguments)]
396    fn convert(
397        &self,
398        path: &str,
399        term: &Term,
400        head: &str,
401        args: &[Term],
402        context: u32,
403        widths: &Widths,
404        bound: &HashMap<&str, (String, u32)>,
405    ) -> Result<(String, u32), Error> {
406        if args.len() != 3 {
407            let said = format!("`{head}` takes two numbers and a value, and this gives it {}", {
408                args.len()
409            });
410            return Err(fail(path, term, said));
411        }
412        // Two numbers, and which two they are depends on the head: the bit positions an extract
413        // takes, and the widths an extension goes between.
414        let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
415
416        if head == "extract" {
417            let (high, low) = (first, second);
418            if high < low {
419                let said = format!("`extract` takes bits {high} down to {low}, which is none");
420                return Err(fail(path, term, said));
421            }
422            let width = widths.scale(high - low + 1);
423            let bottom = widths.index(low);
424            let top = bottom + width - 1;
425            let (text, of) = self.write_at(path, &args[2], context, widths, bound)?;
426            if top >= of {
427                let said = format!(
428                    "`extract` takes bits {top} down to {bottom} of something {of} bits wide"
429                );
430                return Err(fail(path, term, said));
431            }
432            return Ok((format!("((_ extract {top} {bottom}) {text})"), width));
433        }
434
435        let (from, to) = (widths.scale(first), widths.scale(second));
436        if to < from {
437            let said = format!("`{head}` goes from {from} bits to {to}, which is narrower");
438            return Err(fail(path, term, said));
439        }
440        let (text, of) = self.write_at(path, &args[2], from, widths, bound)?;
441        if of != from {
442            let said =
443                format!("`{head}` goes from {from} bits and is given something {of} bits wide");
444            return Err(fail(path, term, said));
445        }
446        // Extending by nothing is written as nothing rather than as an extension by zero,
447        // because a bounded proof can scale two different widths onto the same one.
448        if to == from {
449            return Ok((text, to));
450        }
451        Ok((format!("((_ {head} {}) {text})", to - from), to))
452    }
453}
454
455/// What SMT-LIB calls this head, if it already knows it.
456fn builtin(head: &str) -> Option<&'static str> {
457    BUILTIN.iter().find(|(name, _)| *name == head).map(|(_, smt)| *smt)
458}
459
460/// Whether the solver already knows this head, and so whether the model may not redefine it.
461fn known(head: &str) -> bool {
462    builtin(head).is_some() || CONVERSION.contains(&head)
463}
464
465/// One of the numbers a conversion is written with.
466fn number(path: &str, head: &str, term: &Term) -> Result<u32, Error> {
467    match &term.kind {
468        TermKind::Int(value) => u32::try_from(*value).map_err(|_| {
469            let said = format!("`{head}` is given {value} where it needs a number of bits");
470            fail(path, term, said)
471        }),
472        _ => {
473            let said = format!("`{head}` says which widths it goes between, in numbers");
474            Err(fail(path, term, said))
475        }
476    }
477}
478
479/// A literal at the rule's width. Negative values are written as the bit pattern they are, since
480/// SMT-LIB has no sign on a bitvector literal.
481fn literal(value: i128, width: u32) -> String {
482    let wrapped =
483        if width >= 128 { value as u128 } else { (value as u128) & ((1u128 << width) - 1) };
484    format!("(_ bv{wrapped} {width})")
485}
486
487fn fail(path: &str, term: &Term, message: String) -> Error {
488    Error { path: path.to_owned(), line: term.line, column: term.column, message }
489}