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