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