Skip to main content

rucc_verify/
model.rs

1//! What the terms in a rule mean, in bitvectors and in floats.
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//! # Including another model
21//!
22//! A model may be written on top of another:
23//!
24//! ```text
25//! (include crates/rucc-ir/rules/ir.model)
26//! ```
27//!
28//! There are two rule sets over the IR, the lowering rules of `rucc-codegen` and the rewrite
29//! rules of `rucc-opt`, and both of them need to be told what `add.i32` means. Saying it twice
30//! would be two accounts of one IR with nothing to notice the day they disagreed, so the IR half
31//! is one file and each rule set's model includes it and adds its own heads. A head that two of
32//! the files read together give a meaning to is refused, which is what makes that load bearing.
33//!
34//! The path is counted from the root of the repository rather than from the including file, so
35//! that it reads the same as the path in the prose beside it. [`Model::open`] is what follows an
36//! include, because following one means reading files, and [`Model::read`] takes text and only
37//! remembers that there was one.
38//!
39//! # Widths
40//!
41//! Every term is some number of bits wide and [`Widths`] is what says how many. A head that ends
42//! in `.iN` is N bits wide, anything else is as wide as the term it sits inside, and a name is as
43//! wide as the place in the pattern that bound it. That is enough for a rule to convert between
44//! widths, which is what `sext`, `zext` and `trunc` all are, and those conversions are written
45//! the way `spec/10-backend.md` writes them: `(sign_extend 32 64 x)` and `(extract 31 0 x)`, with
46//! the widths spelled out rather than left to be inferred.
47//!
48//! The widths are checked here rather than left to the solver, because a solver handed two
49//! bitvectors of different sorts says so in its own words and at a place in generated text that
50//! nobody wants to read.
51//!
52//! # Floats
53//!
54//! A head that ends in `.fN` is a float in the interchange format of that many bits, which is not
55//! the bitvector of the same size and is not treated as one: adding two floats is not adding their
56//! bits, and a rule that lowered one to the other would be caught here rather than proved. The
57//! operations are the [`FLOAT`] heads and they are the ones the floating point standard defines,
58//! written with the rounding this file supplies rather than one each rule repeats.
59//!
60//! A bounded proof does not narrow a float. The formats are the four the standard names rather
61//! than a ratio, so a rule about a float is either proved in the format it runs in or not proved,
62//! which is what every rule in the shipped set does anyway.
63//!
64//! The one place a float and the bitvector of the same size are the same thing is [`REINTERPRET`],
65//! which is what a load and a store are: neither instruction looks at the bits it moves. Writing
66//! that as a head of its own is what keeps it from being the default, so a rule that means to read
67//! a float as its bits has to say so and every other way of putting the two together is still an
68//! error.
69//!
70//! The other way across is [`CROSSING`], which is what a conversion instruction does: it reads a
71//! number and writes the float nearest to it, or reads a float and writes the number it stands
72//! for. Those two are as far from a reinterpretation as they could be, since neither keeps a
73//! single bit, and they are written with both widths spelled out for the same reason `sign_extend`
74//! is.
75//!
76//! # Memory
77//!
78//! A rule with an effect is a claim about memory as well as about a value, so not everything a
79//! term computes is a bitvector and [`Sort`] is what says which it is. Memory is one map from an
80//! address to a byte, written as an SMT-LIB array, and the three heads that touch it are
81//! [`MEMORY`]: `(mem)` is the memory a rule starts from, `select` reads one byte of it and
82//! `store` writes one.
83//!
84//! Nothing wider than a byte is built in, which is deliberate. A load of four bytes is four
85//! `select`s put together with `concat` and a store of four bytes is four nested `store`s, both
86//! written out in the model file, so the byte order is a thing a reviewer reads rather than a
87//! thing this file decides on their behalf. That is the one fact about memory access that no
88//! amount of testing on one machine will catch.
89
90use std::collections::{BTreeMap, HashMap};
91use std::fs;
92use std::path::{Path, PathBuf};
93
94use rucc_rules::{Error, Term, TermKind, parse_terms};
95
96/// The heads the solver already understands, and what SMT-LIB calls them.
97///
98/// The comparisons written as symbols are the signed ones. An unsigned comparison in a rule has
99/// to be written with the solver's own name for it, which is deliberate: a rule that means the
100/// unsigned one should have to say so rather than depend on which way this table happens to read.
101/// Both families are here under those names as well, so a rule that would rather be explicit
102/// about the signed one can be.
103const BUILTIN: [(&str, &str); 32] = [
104    ("=", "="),
105    ("and", "and"),
106    ("or", "or"),
107    ("not", "not"),
108    ("<", "bvslt"),
109    ("<=", "bvsle"),
110    (">", "bvsgt"),
111    (">=", "bvsge"),
112    ("bvslt", "bvslt"),
113    ("bvsle", "bvsle"),
114    ("bvsgt", "bvsgt"),
115    ("bvsge", "bvsge"),
116    ("bvult", "bvult"),
117    ("bvule", "bvule"),
118    ("bvugt", "bvugt"),
119    ("bvuge", "bvuge"),
120    ("bvadd", "bvadd"),
121    ("bvsub", "bvsub"),
122    ("bvmul", "bvmul"),
123    ("bvneg", "bvneg"),
124    ("bvnot", "bvnot"),
125    ("bvand", "bvand"),
126    ("bvor", "bvor"),
127    ("bvxor", "bvxor"),
128    ("bvshl", "bvshl"),
129    ("bvlshr", "bvlshr"),
130    ("bvashr", "bvashr"),
131    ("bvsdiv", "bvsdiv"),
132    ("bvudiv", "bvudiv"),
133    ("bvsrem", "bvsrem"),
134    ("bvurem", "bvurem"),
135    ("ite", "ite"),
136];
137
138/// The builtins that take a boolean somewhere, so their arguments are not all one width and
139/// there is nothing to check between them.
140const LOGICAL: [&str; 4] = ["and", "or", "not", "ite"];
141
142/// The heads that work in floats, and how many arguments each takes.
143///
144/// Not in [`BUILTIN`] because SMT-LIB's float arithmetic takes a rounding mode as its first
145/// argument and a rule does not write one. The mode goes in here, once, rather than being a thing
146/// every rule repeats and any rule can get wrong.
147const FLOAT: [(&str, usize); 4] = [("fp.add", 2), ("fp.sub", 2), ("fp.mul", 2), ("fp.div", 2)];
148
149/// The heads that ask a question about floats, and how many arguments each takes.
150///
151/// Separate from [`FLOAT`] because these take no rounding mode: what a comparison answers is the
152/// same whichever way the arithmetic would round, so there is nothing to write in on the rule's
153/// behalf. Separate from [`BUILTIN`] because their arguments are floats and nothing else, which is
154/// the check that catches a rule comparing a float with an instruction that reads bits.
155///
156/// Every one of them is the standard's own comparison rather than the solver's `=`. The two are
157/// not the same relation and the difference is exactly the two cases C programs get wrong: `=`
158/// says a NaN equals itself and says a positive zero is not a negative zero, and `fp.eq` says the
159/// opposite of both, which is what the machine does and what C means by `==`.
160const FLOAT_TEST: [(&str, usize); 6] =
161    [("fp.eq", 2), ("fp.lt", 2), ("fp.leq", 2), ("fp.gt", 2), ("fp.geq", 2), ("fp.isNaN", 1)];
162
163/// The rounding the solver is told to do, which is the one a C program gets unless it asks for
164/// another. `spec/12-abi-and-runtime.md` has the compiler assume the default environment, so the
165/// mode a rule is proved under is the mode the program will run in.
166const ROUNDING: &str = "RNE";
167
168/// The rounding a conversion to an integer does, which is not [`ROUNDING`].
169///
170/// C says a float converted to an integer keeps the part before the point and discards the rest,
171/// whatever the rounding mode is set to, and that is why the instruction is `cvttsd2si` with two
172/// `t`s rather than `cvtsd2si`. A rule proved under the default rounding here would be a rule
173/// proved about the instruction we do not select.
174const TOWARDS_ZERO: &str = "RTZ";
175
176/// The float formats SMT-LIB has a name for, which are the ones a rule may be written in: how
177/// wide each is, then the bits of exponent and the bits of significand SMT-LIB names it by.
178///
179/// The significand counts the bit the format does not store, which is why the three numbers in a
180/// row add up to one more than the width.
181///
182/// Eighty bit is not among them, and that is an answer rather than a gap: the x87 format is not
183/// one of the interchange formats, `crates/rucc-codegen/src/abi.rs` refuses a `long double` on the
184/// same grounds, and a rule about one would have to say what it means rather than borrow a name
185/// from a standard that does not have it.
186const FORMATS: [(u32, u32, u32); 4] = [(16, 5, 11), (32, 8, 24), (64, 11, 53), (128, 15, 113)];
187
188/// The two heads that move between a float and the bits that spell it, which is what a load and a
189/// store of one are: neither instruction looks at what it moves.
190///
191/// Two heads rather than one builtin because they go opposite ways and only one of them is in the
192/// standard theory. Reading bits as a float is SMT-LIB's own `to_fp` on a bitvector. Reading a
193/// float as its bits is not in the theory at all, and `fp.to_ieee_bv` is what a solver that has it
194/// calls it, so the name a rule writes is this file's rather than the solver's for the same reason
195/// a rule writes `<` and the query says `bvslt`.
196const REINTERPRET: [&str; 2] = ["float_from_bits", "bits_from_float"];
197
198/// The heads that go between a float and the number it stands for, which is the other thing an
199/// instruction can do with the two and is the opposite of [`REINTERPRET`]: a conversion keeps the
200/// value as far as it can and keeps no bit, and a reinterpretation keeps every bit and no value.
201///
202/// Each takes the width it comes from, the width it goes to, and the value, in that order, the way
203/// `sign_extend` does. Which of the two widths is a float format and which is a number of bits is
204/// what the name says, and it is checked rather than guessed: `float_from_signed` handed a float
205/// is a rule that has left a conversion out.
206///
207/// Nothing unsigned is here. The machine has no instruction for it below a hundred and twenty
208/// eight bit register, so an unsigned conversion is more than one instruction and belongs in a
209/// pass that rewrites it into these rather than in a rule.
210const CROSSING: [&str; 3] = ["float_from_float", "float_from_signed", "signed_from_float"];
211
212/// The heads that change width. Their first two arguments are widths rather than values, which
213/// is why they are written out here rather than sitting in [`BUILTIN`] with the rest: SMT-LIB
214/// spells them as indexed operators and the index is a number this has to work out.
215const CONVERSION: [&str; 3] = ["sign_extend", "zero_extend", "extract"];
216
217/// The heads that touch memory, which are not in [`BUILTIN`] because their arguments are not all
218/// the same sort and their results are not all the same sort either.
219const MEMORY: [&str; 3] = ["mem", "select", "store"];
220
221/// Putting bitvectors end to end, which is how a load of more than one byte is written. Not in
222/// [`BUILTIN`] because its arguments are one width and its result is their total.
223const CONCAT: &str = "concat";
224
225/// How wide an address is.
226///
227/// Every target `spec/12-abi-and-runtime.md` implements for 1.0 is sixty four bit, so this is a
228/// constant rather than something the model file says. When a thirty two bit target arrives it
229/// becomes something the model file says, and the rules that read memory will be the ones that
230/// notice.
231pub const ADDRESS_WIDTH: u32 = 64;
232
233/// How wide a byte is, which is the element of memory.
234pub const BYTE_WIDTH: u32 = 8;
235
236/// What the memory a rule starts from is called in the query.
237///
238/// A name no rule can bind, because a name in a rule comes out of a pattern and a pattern binds
239/// what the selector matched, which is registers and constants and never memory.
240pub const MEMORY_CONST: &str = "mem";
241
242/// What kind of thing a term computes.
243///
244/// Most things are a bitvector, and the two exceptions are the whole point of this type. A rule
245/// with an effect relates one memory to another, and a memory is not a number however many bits
246/// one is willing to spend on it. A rule about a float relates two floats, and a float is not the
247/// number its bits spell either, however much it looks like one.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum Sort {
250    /// A bitvector this many bits wide.
251    Bits(u32),
252    /// A float in the interchange format of this many bits, which is a different kind of thing
253    /// from the bitvector of the same size: adding two of them is not adding their bits.
254    Float(u32),
255    /// The whole of memory, a map from an address to a byte.
256    Memory,
257}
258
259impl Sort {
260    /// How many bits wide it is, or nothing when it is not a bitvector at all.
261    ///
262    /// A float is not one. Everything that asks this is about to take an extract of it or put it
263    /// end to end with something, and neither is a thing to do to a float without saying so.
264    #[must_use]
265    pub fn bits(self) -> Option<u32> {
266        match self {
267            Sort::Bits(width) => Some(width),
268            Sort::Float(_) | Sort::Memory => None,
269        }
270    }
271
272    /// What SMT-LIB calls it, at the widths this question is being asked at.
273    #[must_use]
274    pub fn write(self, widths: &Widths) -> String {
275        match self {
276            Sort::Bits(width) => format!("(_ BitVec {width})"),
277            // `Float32` and the rest are the names the standard gives the interchange formats,
278            // and [`FLOAT_WIDTHS`] is what keeps this from being asked for a format it has no
279            // name for.
280            Sort::Float(width) => format!("Float{width}"),
281            Sort::Memory => {
282                format!("(Array (_ BitVec {}) (_ BitVec {}))", widths.address(), widths.byte())
283            }
284        }
285    }
286
287    /// How it reads in a message to somebody who has written a rule that does not fit together.
288    pub(crate) fn describe(self) -> String {
289        match self {
290            Sort::Bits(width) => format!("{width} bits wide"),
291            Sort::Float(width) => format!("{width} bits of float"),
292            Sort::Memory => "the whole of memory".to_owned(),
293        }
294    }
295}
296
297/// What a rule works in when its opcode does not say. Every opcode in the IR does say, so this
298/// is what a hand written test rule gets rather than something the real rule set relies on.
299pub const DEFAULT_WIDTH: u32 = 64;
300
301/// How wide each thing in one rule is.
302///
303/// A rule is written at one width, the one its pattern's opcode names, and the terms inside it
304/// may say another: `(value.i64 x)` under an `add.i32` is a thirty two bit add of two sixty four
305/// bit registers, which is the shape every `sext`, `zext` and `trunc` in a lowering has. What a
306/// name stands at is fixed by the pattern, because the pattern is where a name is bound, and
307/// everywhere else reads it from here.
308///
309/// A bounded proof asks the same rule at a narrower width, and that scales every width in the
310/// rule by one ratio rather than flattening them all to one number. A rule that converts between
311/// widths still converts between widths when it is asked at eight bits, which it would not do if
312/// the narrow width were simply substituted everywhere.
313#[derive(Debug, Clone, Default)]
314pub struct Widths {
315    /// The width the rule is written in.
316    natural: u32,
317    /// The width it is being asked at, which is the same number unless this is a bounded proof.
318    asked: u32,
319    /// What each name the pattern binds stands at, already scaled.
320    at: BTreeMap<String, Sort>,
321}
322
323impl Widths {
324    /// The widths one rule's pattern fixes, at the width the rule is written in.
325    #[must_use]
326    pub fn of(pattern: &Term) -> Widths {
327        Widths::at(pattern, rule_width(pattern))
328    }
329
330    /// The same, scaled to a width somebody asked for. This is what a bounded proof is made of.
331    #[must_use]
332    pub fn at(pattern: &Term, asked: u32) -> Widths {
333        let natural = rule_width(pattern);
334        let mut widths = Widths { natural, asked, at: BTreeMap::new() };
335        widths.bind(pattern, Sort::Bits(asked));
336        widths
337    }
338
339    /// The width a term is at when nothing inside it says otherwise.
340    #[must_use]
341    pub fn width(&self) -> u32 {
342        self.asked
343    }
344
345    /// The width the rule is written in, which is the one it will run at.
346    #[must_use]
347    pub fn natural(&self) -> u32 {
348        self.natural
349    }
350
351    /// Every name the pattern binds and what kind of thing it is, sorted.
352    ///
353    /// Sorted rather than in the order the pattern binds them, because the query is something a
354    /// test pins and a diff is easier to read than it is to regenerate.
355    ///
356    /// A memory is not among them. Nothing in a pattern binds one, because a name in a rule comes
357    /// out of what the selector matched and that is registers and constants.
358    pub fn names(&self) -> impl Iterator<Item = (&str, Sort)> {
359        self.at
360            .iter()
361            .filter(|(_, sort)| **sort != Sort::Memory)
362            .map(|(name, sort)| (name.as_str(), *sort))
363    }
364
365    /// These widths and one more name, which is how the replacement's own meaning gets a width
366    /// once it has been substituted into the specification for `(result)`.
367    ///
368    /// A replacement that computes a memory is recorded as one, so that the specification which
369    /// reads it back is checked against a memory rather than against a number of bits nobody
370    /// meant.
371    #[must_use]
372    pub fn with(&self, name: &str, sort: Sort) -> Widths {
373        let mut out = self.clone();
374        out.at.insert(name.to_owned(), sort);
375        out
376    }
377
378    /// How wide an address is here, scaled like everything else.
379    #[must_use]
380    pub fn address(&self) -> u32 {
381        self.scale(ADDRESS_WIDTH)
382    }
383
384    /// How wide a byte is here, scaled like everything else.
385    ///
386    /// A bounded proof asks a rule in narrower bitvectors, and a byte narrows with them. It has
387    /// to: the bytes a load puts together have to add up to the value the load produces, and a
388    /// value that has been scaled and bytes that have not do not add up to anything.
389    #[must_use]
390    pub fn byte(&self) -> u32 {
391        self.scale(BYTE_WIDTH)
392    }
393
394    /// What a name stands for, when the pattern bound it.
395    fn of_name(&self, name: &str) -> Option<Sort> {
396        self.at.get(name).copied()
397    }
398
399    /// The kind of thing a head names, scaled.
400    ///
401    /// A float is not scaled. There is no narrower float to scale to: the formats are the four
402    /// the standard names and they are not a ratio of each other, so a bounded proof of a rule
403    /// about a float asks about the format the rule runs in. That gives up nothing, because the
404    /// claims that need a bounded proof are the ones about wide multiplication and division of
405    /// bitvectors.
406    fn sort_of(&self, head: &str) -> Option<Sort> {
407        match declared(head)? {
408            Sort::Bits(width) => Some(Sort::Bits(self.scale(width))),
409            other => Some(other),
410        }
411    }
412
413    /// The width a head names, when it names a number of bits rather than a float.
414    fn suffix(&self, head: &str) -> Option<u32> {
415        self.sort_of(head).and_then(Sort::bits)
416    }
417
418    /// A width, in the proportion the question is being asked at. Never nothing: a width that
419    /// scales to zero bits is a width the rule cannot be asked about at all.
420    fn scale(&self, width: u32) -> u32 {
421        if self.asked == self.natural || self.natural == 0 {
422            return width;
423        }
424        self.index(width).max(1)
425    }
426
427    /// A bit position, in the same proportion. Zero stays zero, which is what separates this
428    /// from [`Widths::scale`].
429    fn index(&self, position: u32) -> u32 {
430        if self.asked == self.natural || self.natural == 0 {
431            return position;
432        }
433        let scaled = u64::from(position) * u64::from(self.asked) / u64::from(self.natural);
434        u32::try_from(scaled).unwrap_or(position)
435    }
436
437    /// Walk the pattern and write down what each name it binds stands for.
438    fn bind(&mut self, term: &Term, context: Sort) {
439        match &term.kind {
440            TermKind::Var(name) => {
441                self.at.insert(name.clone(), context);
442            }
443            TermKind::Int(_) => {}
444            TermKind::App { head, args } => {
445                let inner = self.sort_of(head).unwrap_or(context);
446                for arg in args {
447                    self.bind(arg, inner);
448                }
449            }
450        }
451    }
452}
453
454/// The width a rule works in, taken from the suffix on its pattern's opcode.
455///
456/// A float rule works in the width of its format, which is the number in the suffix as well.
457/// Nothing scales it, so the only thing that number does for a float rule is stand as the width
458/// any integer term inside it takes when nothing says otherwise.
459#[must_use]
460pub fn rule_width(pattern: &Term) -> u32 {
461    let TermKind::App { head, .. } = &pattern.kind else {
462        return DEFAULT_WIDTH;
463    };
464    match declared(head) {
465        Some(Sort::Bits(width) | Sort::Float(width)) => width,
466        Some(Sort::Memory) | None => DEFAULT_WIDTH,
467    }
468}
469
470/// The kind of thing a head names, if it names one. `add.i32` names a bitvector, `fadd.f32` names
471/// a float, and `x64.lea` names neither.
472fn declared(head: &str) -> Option<Sort> {
473    let (_, suffix) = head.rsplit_once('.')?;
474    let number = |kind: char| suffix.strip_prefix(kind).and_then(|bits| bits.parse::<u32>().ok());
475    if let Some(bits) = number('i') {
476        return Some(Sort::Bits(bits));
477    }
478    let bits = number('f')?;
479    format_of(bits).map(|_| Sort::Float(bits))
480}
481
482/// The two numbers SMT-LIB names a float format by, if that width is one of the formats it names.
483fn format_of(width: u32) -> Option<(u32, u32)> {
484    FORMATS
485        .iter()
486        .find(|(bits, _, _)| *bits == width)
487        .map(|(_, exponent, significand)| (*exponent, *significand))
488}
489
490/// Read one file into a model, then everything it includes.
491///
492/// `blame` is the include that asked for this file, and the file that wrote it, so that a
493/// problem with the file itself is reported where somebody asked for it rather than at the
494/// first line of a file that may not be there. Nothing asked for the file somebody named on the
495/// command line, which is the case where there is nowhere else to point.
496fn absorb(
497    path: &Path,
498    blame: Option<(&str, &Included)>,
499    model: &mut Model,
500    read: &mut Vec<PathBuf>,
501    defined: &mut HashMap<String, String>,
502    errors: &mut Vec<Error>,
503) {
504    let shown = path.display().to_string();
505    let here = |message: String| match blame {
506        Some((asked, at)) => {
507            Error { path: asked.to_owned(), line: at.line, column: at.column, message }
508        }
509        None => Error { path: shown.clone(), line: 1, column: 1, message },
510    };
511
512    let full = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
513    // Reading a file twice would be every head in it given a meaning twice, and a cycle would
514    // not stop at all. Two rule sets over one IR are two models including one file, so this is
515    // the normal case rather than something to report.
516    if read.contains(&full) {
517        return;
518    }
519    read.push(full.clone());
520
521    let text = match fs::read_to_string(path) {
522        Ok(text) => text,
523        Err(problem) => {
524            errors.push(here(format!("{shown} cannot be read: {problem}")));
525            return;
526        }
527    };
528    let one = match Model::read(&shown, &text) {
529        Ok(one) => one,
530        Err(mut found) => {
531            errors.append(&mut found);
532            return;
533        }
534    };
535
536    // Sorted, because a map has no order and two runs of a gate that disagree about the order
537    // they say things in are two runs somebody has to diff by hand.
538    let mut heads: Vec<(String, Meaning)> = one.heads.into_iter().collect();
539    heads.sort_by(|(left, _), (right, _)| left.cmp(right));
540    for (name, meaning) in heads {
541        let (line, column) = (meaning.body.line, meaning.body.column);
542        model.heads.insert(name.clone(), meaning);
543        if let Some(already) = defined.insert(name.clone(), shown.clone()) {
544            let said = format!("`{name}` is given a meaning here and in {already}");
545            errors.push(Error { path: shown.clone(), line, column, message: said });
546        }
547    }
548
549    let Some(root) = root_above(&full) else {
550        if !one.includes.is_empty() {
551            let said = format!("{shown} includes a file, and nothing above it is a workspace");
552            errors.push(here(said));
553        }
554        return;
555    };
556    for include in &one.includes {
557        absorb(&root.join(&include.path), Some((&shown, include)), model, read, defined, errors);
558    }
559}
560
561/// The root of the repository a file is in, which is the first directory above it whose
562/// `Cargo.toml` says it is a workspace.
563///
564/// An include names a file from there rather than from wherever the including file happens to
565/// sit, so this is what turns the one into the other.
566fn root_above(from: &Path) -> Option<PathBuf> {
567    from.ancestors().skip(1).find_map(|dir| {
568        let manifest = fs::read_to_string(dir.join("Cargo.toml")).ok()?;
569        manifest.contains("[workspace]").then(|| dir.to_path_buf())
570    })
571}
572
573/// What one head means.
574#[derive(Debug, Clone)]
575struct Meaning {
576    /// The names the body is written in terms of.
577    params: Vec<String>,
578    /// What it computes.
579    body: Term,
580}
581
582/// A model this one is written on top of, and where it said so.
583#[derive(Debug, Clone)]
584struct Included {
585    /// The file, named from the root of the repository the way everything else here names one.
586    path: String,
587    /// The line the `(include ...)` is on, so that a file that is not there is reported where
588    /// somebody asked for it.
589    line: u32,
590    /// The column, for the same reason.
591    column: u32,
592}
593
594/// Everything the rules are allowed to say, and what each of it means.
595#[derive(Debug, Default)]
596pub struct Model {
597    heads: HashMap<String, Meaning>,
598    includes: Vec<Included>,
599}
600
601impl Model {
602    /// Read a model from a file, and every model it is written on top of.
603    ///
604    /// An include names a file from the root of the repository, which is the first directory
605    /// above the including one whose `Cargo.toml` says it is a workspace. Naming it that way
606    /// rather than relative to whoever wrote the include is what makes the path in an include
607    /// read the same as the path in the prose beside it, since everything else in this
608    /// repository names a file from the root.
609    ///
610    /// A file included twice is read once. That is the normal case rather than a mistake, since
611    /// two rule sets over the same IR are two models including one file, and it is also what
612    /// stops a cycle.
613    ///
614    /// # Errors
615    ///
616    /// Everything [`Model::read`] refuses, plus a file that is not there, a repository root that
617    /// cannot be found, and a head that two of the files give a meaning to.
618    pub fn open(path: &Path) -> Result<Model, Vec<Error>> {
619        let mut model = Model::default();
620        let mut read = Vec::new();
621        let mut defined = HashMap::new();
622        let mut errors = Vec::new();
623        absorb(path, None, &mut model, &mut read, &mut defined, &mut errors);
624        if errors.is_empty() { Ok(model) } else { Err(errors) }
625    }
626
627    /// Read a model from text.
628    ///
629    /// What the text includes is remembered rather than followed, because following it means
630    /// reading files and this takes text. [`Model::open`] is the one that reads files.
631    ///
632    /// # Errors
633    ///
634    /// Anything that is not a well formed `(semantics (head params) body)` form or a well formed
635    /// `(include path)` form, and any head given a meaning twice.
636    pub fn read(path: &str, text: &str) -> Result<Model, Vec<Error>> {
637        let terms = parse_terms(path, text)?;
638        let mut model = Model::default();
639        let mut errors = Vec::new();
640
641        for term in terms {
642            let TermKind::App { head, args } = &term.kind else {
643                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
644                continue;
645            };
646            if head == "include" {
647                match args.first().map(|arg| &arg.kind) {
648                    Some(TermKind::Var(named)) if args.len() == 1 => {
649                        model.includes.push(Included {
650                            path: named.clone(),
651                            line: term.line,
652                            column: term.column,
653                        });
654                    }
655                    _ => {
656                        let said = "an include names one file, from the root of the repository";
657                        errors.push(fail(path, &term, said.to_owned()));
658                    }
659                }
660                continue;
661            }
662            if head != "semantics" || args.len() != 2 {
663                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
664                continue;
665            }
666            let TermKind::App { head: name, args: params } = &args[0].kind else {
667                errors.push(fail(path, &args[0], "expected a head and its parameters".to_owned()));
668                continue;
669            };
670            let mut names = Vec::new();
671            for param in params {
672                match &param.kind {
673                    TermKind::Var(name) => names.push(name.clone()),
674                    _ => errors.push(fail(path, param, "a parameter has to be a name".to_owned())),
675                }
676            }
677            if known(name) {
678                let said = format!("`{name}` is something the solver already knows");
679                errors.push(fail(path, &args[0], said));
680                continue;
681            }
682            let meaning = Meaning { params: names, body: args[1].clone() };
683            if model.heads.insert(name.clone(), meaning).is_some() {
684                let said = format!("`{name}` is given a meaning twice");
685                errors.push(fail(path, &args[0], said));
686            }
687        }
688
689        if errors.is_empty() { Ok(model) } else { Err(errors) }
690    }
691
692    /// Whether this model gives a head a meaning.
693    ///
694    /// What a rule needs is [`Model::write`], which expands a whole term. This is for anything
695    /// asking about one head on its own, which is a test and a message about a head with no
696    /// entry anywhere.
697    #[must_use]
698    pub fn knows(&self, head: &str) -> bool {
699        self.heads.contains_key(head)
700    }
701
702    /// Write one term out as SMT-LIB, expanding everything the model defines, and say how wide
703    /// what it computes is.
704    ///
705    /// # Errors
706    ///
707    /// A head that is neither a builtin nor in the model, since that is a term nobody has said
708    /// the meaning of, an application of the wrong number of arguments, and anything whose
709    /// widths do not fit together.
710    pub fn write(&self, path: &str, term: &Term, widths: &Widths) -> Result<(String, Sort), Error> {
711        self.write_at(path, term, widths.width(), widths, &HashMap::new())
712    }
713
714    /// Whether reading this term reaches memory, following every head the model defines.
715    ///
716    /// A rule that reads memory needs a solver told about arrays and a constant to stand for the
717    /// memory it starts from, and neither is worth putting in a query that does not. Nothing in a
718    /// rule says `(mem)` directly: a load says `load.i32`, and it is the model entry for that head
719    /// which reaches memory, so this expands what the model says rather than reading the surface.
720    #[must_use]
721    pub fn touches_memory(&self, term: &Term) -> bool {
722        match &term.kind {
723            TermKind::Var(_) | TermKind::Int(_) => false,
724            TermKind::App { head, args } => {
725                if MEMORY.contains(&head.as_str()) {
726                    return true;
727                }
728                if args.iter().any(|arg| self.touches_memory(arg)) {
729                    return true;
730                }
731                self.heads.get(head).is_some_and(|meaning| self.touches_memory(&meaning.body))
732            }
733        }
734    }
735
736    /// Whether reading this term reaches a float, following every head the model defines.
737    ///
738    /// A rule that does needs a solver told about floats, and a solver told about floats is
739    /// slower at every rule that has none, so the question is worth asking rather than answering
740    /// yes for the whole file. A head is a float either by its own suffix, as `fadd.f32` is, or
741    /// by what the model says it means.
742    #[must_use]
743    pub fn touches_floats(&self, term: &Term) -> bool {
744        match &term.kind {
745            TermKind::Var(_) | TermKind::Int(_) => false,
746            TermKind::App { head, args } => {
747                if float_op(head).is_some() || float_test(head).is_some() {
748                    return true;
749                }
750                if matches!(declared(head), Some(Sort::Float(_))) {
751                    return true;
752                }
753                if REINTERPRET.contains(&head.as_str()) || CROSSING.contains(&head.as_str()) {
754                    return true;
755                }
756                if args.iter().any(|arg| self.touches_floats(arg)) {
757                    return true;
758                }
759                self.heads.get(head).is_some_and(|meaning| self.touches_floats(&meaning.body))
760            }
761        }
762    }
763
764    fn write_at(
765        &self,
766        path: &str,
767        term: &Term,
768        context: u32,
769        widths: &Widths,
770        bound: &HashMap<&str, (String, Sort)>,
771    ) -> Result<(String, Sort), Error> {
772        match &term.kind {
773            TermKind::Var(name) => match bound.get(name.as_str()) {
774                Some((already, sort)) => Ok((already.clone(), *sort)),
775                None => Ok((name.clone(), widths.of_name(name).unwrap_or(Sort::Bits(context)))),
776            },
777            TermKind::Int(value) => Ok((literal(*value, context), Sort::Bits(context))),
778            TermKind::App { head, args } => {
779                if CONVERSION.contains(&head.as_str()) {
780                    return self.convert(path, term, head, args, context, widths, bound);
781                }
782                if MEMORY.contains(&head.as_str()) {
783                    return self.reach(path, term, head, args, context, widths, bound);
784                }
785                if head == CONCAT {
786                    return self.join(path, term, args, context, widths, bound);
787                }
788                if let Some(name) = builtin(head) {
789                    return self.combine(path, term, head, name, args, context, widths, bound);
790                }
791                if let Some(takes) = float_op(head) {
792                    return self.rounded(path, term, head, takes, args, context, widths, bound);
793                }
794                if let Some(takes) = float_test(head) {
795                    return self.asking(path, term, head, takes, args, context, widths, bound);
796                }
797                if REINTERPRET.contains(&head.as_str()) {
798                    return self.reinterpret(path, term, head, args, widths, bound);
799                }
800                if CROSSING.contains(&head.as_str()) {
801                    return self.crossing(path, term, head, args, widths, bound);
802                }
803                let own = widths.suffix(head).unwrap_or(context);
804                let mut written = Vec::with_capacity(args.len());
805                for arg in args {
806                    written.push(self.write_at(path, arg, own, widths, bound)?);
807                }
808                let Some(meaning) = self.heads.get(head) else {
809                    let said = format!("nothing in the model says what `{head}` means");
810                    return Err(fail(path, term, said));
811                };
812                if meaning.params.len() != written.len() {
813                    let said = format!(
814                        "`{head}` means something with {} arguments and this gives it {}",
815                        meaning.params.len(),
816                        written.len()
817                    );
818                    return Err(fail(path, term, said));
819                }
820                let inner: HashMap<&str, (String, Sort)> =
821                    meaning.params.iter().map(String::as_str).zip(written).collect();
822                let (text, sort) = self.write_at(path, &meaning.body, own, widths, &inner)?;
823                // An opcode that names a width has to mean something that wide. This is the
824                // model being held to what the rules say about it: `add.i32` over registers
825                // that are sixty four bits wide means an add of their low halves, and a model
826                // that leaves the truncation out says so here rather than in a proof that
827                // quietly asks the wrong question.
828                //
829                // A head that means a memory is the one exception, and it is not a hole. The
830                // width on `store.i32` is the width of what it wrote rather than of what it
831                // computes, and that width is checked all the same, by the extracts in the
832                // model entry having to come out of something that wide.
833                if let Some(said) = widths.sort_of(head).filter(|_| sort != Sort::Memory) {
834                    let agrees = match (said, sort) {
835                        (Sort::Bits(a), Sort::Bits(b)) | (Sort::Float(a), Sort::Float(b)) => a == b,
836                        _ => false,
837                    };
838                    if !agrees {
839                        let told = match (said, sort) {
840                            (Sort::Bits(said), Sort::Bits(width)) => format!(
841                                "`{head}` is written for {said} bits and means something {width} \
842                                 bits wide"
843                            ),
844                            _ => format!(
845                                "`{head}` is written for something {} and means something {}",
846                                said.describe(),
847                                sort.describe()
848                            ),
849                        };
850                        return Err(fail(path, term, told));
851                    }
852                }
853                Ok((text, sort))
854            }
855        }
856    }
857
858    /// One of the heads the solver already knows, applied to arguments that all have to be the
859    /// same width unless a boolean is involved.
860    #[allow(clippy::too_many_arguments)]
861    fn combine(
862        &self,
863        path: &str,
864        term: &Term,
865        head: &str,
866        name: &str,
867        args: &[Term],
868        context: u32,
869        widths: &Widths,
870        bound: &HashMap<&str, (String, Sort)>,
871    ) -> Result<(String, Sort), Error> {
872        // A number has no width of its own and takes the width of what it sits beside. Every
873        // rule written before memory arrived had one width throughout, so this changed nothing
874        // for them, and it is what lets an offset added to an address in the model be as wide as
875        // the address rather than as wide as the value being loaded through it.
876        //
877        // Not under a head that takes a boolean. What a number sits beside there is a
878        // comparison, and a comparison has no width to lend: the one and the zero an `ite`
879        // chooses between are as wide as the term the `ite` is in, which is what `context` is.
880        let beside = if LOGICAL.contains(&head) {
881            context
882        } else {
883            self.beside(path, args, context, widths, bound)?
884        };
885        let mut written = Vec::with_capacity(args.len());
886        for arg in args {
887            let at = if matches!(arg.kind, TermKind::Int(_)) { beside } else { context };
888            written.push(self.write_at(path, arg, at, widths, bound)?);
889        }
890        let Some((_, first)) = written.first() else {
891            return Err(fail(path, term, format!("`{head}` needs arguments")));
892        };
893        let first = *first;
894        if !LOGICAL.contains(&head) {
895            // A head the solver spells with `bv` is arithmetic on bits, and handing it a float
896            // is the mistake a rule makes when it lowers float arithmetic to an integer
897            // instruction. The two are the same number of bits and nothing else about them is
898            // the same, so this is caught here rather than left to come back as a proof.
899            if name.starts_with("bv") && !matches!(first, Sort::Bits(_)) {
900                let said = format!("`{head}` works on bitvectors and this is {}", first.describe());
901                return Err(fail(path, term, said));
902            }
903            if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
904                let said = format!(
905                    "`{head}` is given something {} and something {}, and those are not the \
906                     same kind of thing",
907                    first.describe(),
908                    other.describe()
909                );
910                return Err(fail(path, term, said));
911            }
912        }
913        // A comparison computes a boolean and its width is nobody's business, so saying it is
914        // as wide as what it compared costs nothing and keeps every term having an answer.
915        let sort = if head == "ite" && written.len() > 1 { written[1].1 } else { first };
916        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
917        Ok((format!("({name} {})", texts.join(" ")), sort))
918    }
919
920    /// One of the float operations, whose arguments are all one format and whose result is that
921    /// format, with the rounding written in on the rule's behalf.
922    ///
923    /// A number is not one of the things this takes. There is no reading of a bitvector literal
924    /// as a float that does not have to say which reading it is, so a rule that wants a constant
925    /// float says so with a head of its own rather than by writing a number here.
926    #[allow(clippy::too_many_arguments)]
927    fn rounded(
928        &self,
929        path: &str,
930        term: &Term,
931        head: &str,
932        takes: usize,
933        args: &[Term],
934        context: u32,
935        widths: &Widths,
936        bound: &HashMap<&str, (String, Sort)>,
937    ) -> Result<(String, Sort), Error> {
938        if args.len() != takes {
939            let said = format!("`{head}` takes {takes} arguments and this gives it {}", args.len());
940            return Err(fail(path, term, said));
941        }
942        let mut written = Vec::with_capacity(args.len());
943        for arg in args {
944            written.push(self.write_at(path, arg, context, widths, bound)?);
945        }
946        let first = written[0].1;
947        if !matches!(first, Sort::Float(_)) {
948            let said = format!("`{head}` works on floats and this is {}", first.describe());
949            return Err(fail(path, &args[0], said));
950        }
951        if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
952            let said = format!(
953                "`{head}` is given something {} and something {}, and those are not the same \
954                 kind of thing",
955                first.describe(),
956                other.describe()
957            );
958            return Err(fail(path, term, said));
959        }
960        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
961        Ok((format!("({head} {ROUNDING} {})", texts.join(" ")), first))
962    }
963
964    /// One of the questions asked about floats, whose arguments are all one format and whose
965    /// answer is a boolean.
966    ///
967    /// No rounding, because none of these rounds anything: whether one float is less than another
968    /// is settled before any rounding could apply, and SMT-LIB spells them without a mode for that
969    /// reason.
970    ///
971    /// The sort it gives back is the format it was handed rather than anything about a boolean,
972    /// which is the same shape [`Model::combine`] gives a bitvector comparison and is there for the
973    /// same reason: what a boolean is wide is nobody's question, and the one place the answer is
974    /// read is the `ite` above it, which takes its width from the branches instead.
975    #[allow(clippy::too_many_arguments)]
976    fn asking(
977        &self,
978        path: &str,
979        term: &Term,
980        head: &str,
981        takes: usize,
982        args: &[Term],
983        context: u32,
984        widths: &Widths,
985        bound: &HashMap<&str, (String, Sort)>,
986    ) -> Result<(String, Sort), Error> {
987        if args.len() != takes {
988            let said = format!("`{head}` takes {takes} arguments and this gives it {}", args.len());
989            return Err(fail(path, term, said));
990        }
991        let mut written = Vec::with_capacity(args.len());
992        for arg in args {
993            written.push(self.write_at(path, arg, context, widths, bound)?);
994        }
995        let first = written[0].1;
996        if !matches!(first, Sort::Float(_)) {
997            let said = format!("`{head}` asks about floats and this is {}", first.describe());
998            return Err(fail(path, &args[0], said));
999        }
1000        if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
1001            let said = format!(
1002                "`{head}` is given something {} and something {}, and a comparison is between two \
1003                 of one format",
1004                first.describe(),
1005                other.describe()
1006            );
1007            return Err(fail(path, term, said));
1008        }
1009        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
1010        Ok((format!("({head} {})", texts.join(" ")), first))
1011    }
1012
1013    /// A float read as the bits that spell it, or the bits read back as the float, which is the
1014    /// one place here where the two are the same thing.
1015    ///
1016    /// The format is written out rather than taken from what is inside, for the reason
1017    /// `spec/10-backend.md` gives about every other conversion: a rule that changes what kind of
1018    /// thing it is holding should say what it is changing it into, and a reader should not have
1019    /// to work out the answer from somewhere else in the term.
1020    ///
1021    /// Nothing here is scaled. A float format is one of four the standard names rather than a
1022    /// ratio, so the bits that spell one are as fixed as the format is, and a bounded proof of a
1023    /// rule that read memory into a float would scale the bytes, leave the format alone and be
1024    /// told the two no longer fit.
1025    fn reinterpret(
1026        &self,
1027        path: &str,
1028        term: &Term,
1029        head: &str,
1030        args: &[Term],
1031        widths: &Widths,
1032        bound: &HashMap<&str, (String, Sort)>,
1033    ) -> Result<(String, Sort), Error> {
1034        if args.len() != 2 {
1035            let said =
1036                format!("`{head}` takes a format and a value, and this gives it {}", args.len());
1037            return Err(fail(path, term, said));
1038        }
1039        let width = number(path, head, &args[0])?;
1040        let Some((exponent, significand)) = format_of(width) else {
1041            let said = format!("`{head}` is written at {width} bits, which is not a float format");
1042            return Err(fail(path, term, said));
1043        };
1044        let into_float = head == "float_from_bits";
1045        let (text, sort) = self.write_at(path, &args[1], width, widths, bound)?;
1046        let wanted = if into_float { Sort::Bits(width) } else { Sort::Float(width) };
1047        if sort != wanted {
1048            let said = format!(
1049                "`{head}` takes something {} and this is {}",
1050                wanted.describe(),
1051                sort.describe()
1052            );
1053            return Err(fail(path, &args[1], said));
1054        }
1055        if into_float {
1056            // SMT-LIB's own operator, whose one bitvector argument is the reading that changes
1057            // no bits. The other readings of `to_fp` take a rounding mode and a value, and this
1058            // is not one of them.
1059            let said = format!("((_ to_fp {exponent} {significand}) {text})");
1060            return Ok((said, Sort::Float(width)));
1061        }
1062        Ok((format!("(fp.to_ieee_bv {text})"), Sort::Bits(width)))
1063    }
1064
1065    /// A value carried from one format to another, or between a float and the number it stands
1066    /// for, which is what the conversion instructions do.
1067    ///
1068    /// The rounding is not the same on the way in as on the way out. Going to a float rounds to
1069    /// nearest, which is the mode a C program runs in unless it asks for another. Going to an
1070    /// integer cuts towards zero whatever the mode says, because that is what C means by the
1071    /// conversion and it is why the instruction has two `t`s in its name.
1072    ///
1073    /// A float too big for the integer it is asked for has no answer here, and that is right
1074    /// rather than missing. SMT-LIB leaves `fp.to_sbv` unspecified outside the range, C leaves the
1075    /// conversion undefined there, and the machine writes a value of its own choosing. A rule
1076    /// about one is proved for every float the conversion is defined for and claims nothing about
1077    /// the rest, which is the strongest true claim there is.
1078    fn crossing(
1079        &self,
1080        path: &str,
1081        term: &Term,
1082        head: &str,
1083        args: &[Term],
1084        widths: &Widths,
1085        bound: &HashMap<&str, (String, Sort)>,
1086    ) -> Result<(String, Sort), Error> {
1087        if args.len() != 3 {
1088            let said = format!(
1089                "`{head}` takes the width it comes from, the width it goes to and a value, and \
1090                 this gives it {}",
1091                args.len()
1092            );
1093            return Err(fail(path, term, said));
1094        }
1095        let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
1096        let from_float = head != "float_from_signed";
1097        let into_float = head != "signed_from_float";
1098        let float_format = |width: u32| {
1099            format_of(width).ok_or_else(|| {
1100                let said = format!("`{head}` is written at {width} bits, which is not a format");
1101                fail(path, term, said)
1102            })
1103        };
1104
1105        // The float side is written at the width the format is, since a format is one of four the
1106        // standard names rather than a ratio of anything. The number side scales the way every
1107        // other bitvector in a bounded proof does, so a rule asked at a narrower width is a rule
1108        // about converting to a narrower integer and is still a rule about a conversion.
1109        let from = if from_float { first } else { widths.scale(first) };
1110        let to = if into_float { second } else { widths.scale(second) };
1111        let wanted = if from_float {
1112            float_format(from)?;
1113            Sort::Float(from)
1114        } else {
1115            Sort::Bits(from)
1116        };
1117        let (text, sort) = self.write_at(path, &args[2], from, widths, bound)?;
1118        if sort != wanted {
1119            let said = format!(
1120                "`{head}` takes something {} and this is {}",
1121                wanted.describe(),
1122                sort.describe()
1123            );
1124            return Err(fail(path, &args[2], said));
1125        }
1126        if into_float {
1127            let (exponent, significand) = float_format(to)?;
1128            let said = format!("((_ to_fp {exponent} {significand}) {ROUNDING} {text})");
1129            return Ok((said, Sort::Float(to)));
1130        }
1131        Ok((format!("((_ fp.to_sbv {to}) {TOWARDS_ZERO} {text})"), Sort::Bits(to)))
1132    }
1133
1134    /// The width the numbers among a head's arguments should take, which is the width of the
1135    /// first argument that has one of its own. Nothing when they are all numbers, in which case
1136    /// the surrounding width is as good an answer as there is.
1137    fn beside(
1138        &self,
1139        path: &str,
1140        args: &[Term],
1141        context: u32,
1142        widths: &Widths,
1143        bound: &HashMap<&str, (String, Sort)>,
1144    ) -> Result<u32, Error> {
1145        if !args.iter().any(|arg| matches!(arg.kind, TermKind::Int(_))) {
1146            return Ok(context);
1147        }
1148        let Some(sized) = args.iter().find(|arg| !matches!(arg.kind, TermKind::Int(_))) else {
1149            return Ok(context);
1150        };
1151        let (_, sort) = self.write_at(path, sized, context, widths, bound)?;
1152        Ok(sort.bits().unwrap_or(context))
1153    }
1154
1155    /// One of the three heads that touch memory.
1156    #[allow(clippy::too_many_arguments)]
1157    fn reach(
1158        &self,
1159        path: &str,
1160        term: &Term,
1161        head: &str,
1162        args: &[Term],
1163        context: u32,
1164        widths: &Widths,
1165        bound: &HashMap<&str, (String, Sort)>,
1166    ) -> Result<(String, Sort), Error> {
1167        // The memory a rule starts from, which is one constant and takes no arguments. It is
1168        // written `(mem)` for the reason `(result)` is: a head applied to nothing is still an
1169        // application, because a bare name is a variable.
1170        if head == "mem" {
1171            if !args.is_empty() {
1172                let said = "`mem` is the memory a rule starts from and takes nothing".to_owned();
1173                return Err(fail(path, term, said));
1174            }
1175            return Ok((MEMORY_CONST.to_owned(), Sort::Memory));
1176        }
1177
1178        let wanted = if head == "select" { 2 } else { 3 };
1179        if args.len() != wanted {
1180            let said =
1181                format!("`{head}` takes {wanted} arguments and this gives it {}", args.len());
1182            return Err(fail(path, term, said));
1183        }
1184        let mut written = Vec::with_capacity(args.len());
1185        for arg in args {
1186            let at = if matches!(arg.kind, TermKind::Int(_)) { widths.address() } else { context };
1187            written.push(self.write_at(path, arg, at, widths, bound)?);
1188        }
1189        // The sorts of the three positions, which is the whole of what an array is: a memory, an
1190        // address into it, and for a store the byte that goes there.
1191        let expected = [Sort::Memory, Sort::Bits(widths.address()), Sort::Bits(widths.byte())];
1192        for (at, (_, got)) in written.iter().enumerate() {
1193            if *got != expected[at] {
1194                let said = format!(
1195                    "`{head}` takes something {} in position {at} and this is {}",
1196                    expected[at].describe(),
1197                    got.describe()
1198                );
1199                return Err(fail(path, term, said));
1200            }
1201        }
1202        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
1203        let sort = if head == "select" { Sort::Bits(widths.byte()) } else { Sort::Memory };
1204        Ok((format!("({head} {})", texts.join(" ")), sort))
1205    }
1206
1207    /// Bitvectors end to end, which is as wide as all of them together.
1208    ///
1209    /// The first argument is the high end, which is how SMT-LIB reads it and is the opposite of
1210    /// the order the bytes of a little endian load are at in memory. That is why a load in the
1211    /// model file counts down.
1212    fn join(
1213        &self,
1214        path: &str,
1215        term: &Term,
1216        args: &[Term],
1217        context: u32,
1218        widths: &Widths,
1219        bound: &HashMap<&str, (String, Sort)>,
1220    ) -> Result<(String, Sort), Error> {
1221        if args.len() < 2 {
1222            let said = format!("`concat` puts two or more things together and this gives it {}", {
1223                args.len()
1224            });
1225            return Err(fail(path, term, said));
1226        }
1227        let mut total = 0;
1228        let mut texts = Vec::with_capacity(args.len());
1229        for arg in args {
1230            let (text, sort) = self.write_at(path, arg, context, widths, bound)?;
1231            let Some(width) = sort.bits() else {
1232                let said = "`concat` puts bitvectors together and this is a memory".to_owned();
1233                return Err(fail(path, arg, said));
1234            };
1235            total += width;
1236            texts.push(text);
1237        }
1238        Ok((format!("(concat {})", texts.join(" ")), Sort::Bits(total)))
1239    }
1240
1241    /// A conversion between widths, written as `spec/10-backend.md` writes it, with the widths
1242    /// as arguments rather than inferred from anything.
1243    #[allow(clippy::too_many_arguments)]
1244    fn convert(
1245        &self,
1246        path: &str,
1247        term: &Term,
1248        head: &str,
1249        args: &[Term],
1250        context: u32,
1251        widths: &Widths,
1252        bound: &HashMap<&str, (String, Sort)>,
1253    ) -> Result<(String, Sort), Error> {
1254        if args.len() != 3 {
1255            let said = format!("`{head}` takes two numbers and a value, and this gives it {}", {
1256                args.len()
1257            });
1258            return Err(fail(path, term, said));
1259        }
1260        // Two numbers, and which two they are depends on the head: the bit positions an extract
1261        // takes, and the widths an extension goes between.
1262        let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
1263
1264        if head == "extract" {
1265            let (high, low) = (first, second);
1266            if high < low {
1267                let said = format!("`extract` takes bits {high} down to {low}, which is none");
1268                return Err(fail(path, term, said));
1269            }
1270            let width = widths.scale(high - low + 1);
1271            let bottom = widths.index(low);
1272            let top = bottom + width - 1;
1273            let (text, sort) = self.write_at(path, &args[2], context, widths, bound)?;
1274            let of = bits(path, head, &args[2], sort)?;
1275            if top >= of {
1276                let said = format!(
1277                    "`extract` takes bits {top} down to {bottom} of something {of} bits wide"
1278                );
1279                return Err(fail(path, term, said));
1280            }
1281            return Ok((format!("((_ extract {top} {bottom}) {text})"), Sort::Bits(width)));
1282        }
1283
1284        let (from, to) = (widths.scale(first), widths.scale(second));
1285        if to < from {
1286            let said = format!("`{head}` goes from {from} bits to {to}, which is narrower");
1287            return Err(fail(path, term, said));
1288        }
1289        let (text, sort) = self.write_at(path, &args[2], from, widths, bound)?;
1290        let of = bits(path, head, &args[2], sort)?;
1291        if of != from {
1292            let said =
1293                format!("`{head}` goes from {from} bits and is given something {of} bits wide");
1294            return Err(fail(path, term, said));
1295        }
1296        // Extending by nothing is written as nothing rather than as an extension by zero,
1297        // because a bounded proof can scale two different widths onto the same one.
1298        if to == from {
1299            return Ok((text, Sort::Bits(to)));
1300        }
1301        Ok((format!("((_ {head} {}) {text})", to - from), Sort::Bits(to)))
1302    }
1303}
1304
1305/// What SMT-LIB calls this head, if it already knows it.
1306fn builtin(head: &str) -> Option<&'static str> {
1307    BUILTIN.iter().find(|(name, _)| *name == head).map(|(_, smt)| *smt)
1308}
1309
1310/// How many arguments this float operation takes, if it is one.
1311fn float_op(head: &str) -> Option<usize> {
1312    FLOAT.iter().find(|(name, _)| *name == head).map(|(_, takes)| *takes)
1313}
1314
1315/// How many arguments this float question takes, if it is one.
1316fn float_test(head: &str) -> Option<usize> {
1317    FLOAT_TEST.iter().find(|(name, _)| *name == head).map(|(_, takes)| *takes)
1318}
1319
1320/// Whether the solver already knows this head, and so whether the model may not redefine it.
1321fn known(head: &str) -> bool {
1322    builtin(head).is_some()
1323        || float_op(head).is_some()
1324        || float_test(head).is_some()
1325        || REINTERPRET.contains(&head)
1326        || CROSSING.contains(&head)
1327        || CONVERSION.contains(&head)
1328        || MEMORY.contains(&head)
1329        || head == CONCAT
1330}
1331
1332/// How wide something is, when it has to be a bitvector and the rule is wrong if it is not.
1333fn bits(path: &str, head: &str, term: &Term, sort: Sort) -> Result<u32, Error> {
1334    sort.bits().ok_or_else(|| {
1335        let said = format!("`{head}` works on bitvectors and this is {}", sort.describe());
1336        fail(path, term, said)
1337    })
1338}
1339
1340/// One of the numbers a conversion is written with.
1341fn number(path: &str, head: &str, term: &Term) -> Result<u32, Error> {
1342    match &term.kind {
1343        TermKind::Int(value) => u32::try_from(*value).map_err(|_| {
1344            let said = format!("`{head}` is given {value} where it needs a number of bits");
1345            fail(path, term, said)
1346        }),
1347        _ => {
1348            let said = format!("`{head}` says which widths it goes between, in numbers");
1349            Err(fail(path, term, said))
1350        }
1351    }
1352}
1353
1354/// A literal at the rule's width. Negative values are written as the bit pattern they are, since
1355/// SMT-LIB has no sign on a bitvector literal.
1356fn literal(value: i128, width: u32) -> String {
1357    let wrapped =
1358        if width >= 128 { value as u128 } else { (value as u128) & ((1u128 << width) - 1) };
1359    format!("(_ bv{wrapped} {width})")
1360}
1361
1362fn fail(path: &str, term: &Term, message: String) -> Error {
1363    Error { path: path.to_owned(), line: term.line, column: term.column, message }
1364}