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