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