Skip to main content

rucc_verify/
model.rs

1//! What the terms in a rule mean, in bitvectors.
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//! # Memory
34//!
35//! A rule with an effect is a claim about memory as well as about a value, so not everything a
36//! term computes is a bitvector and [`Sort`] is what says which it is. Memory is one map from an
37//! address to a byte, written as an SMT-LIB array, and the three heads that touch it are
38//! [`MEMORY`]: `(mem)` is the memory a rule starts from, `select` reads one byte of it and
39//! `store` writes one.
40//!
41//! Nothing wider than a byte is built in, which is deliberate. A load of four bytes is four
42//! `select`s put together with `concat` and a store of four bytes is four nested `store`s, both
43//! written out in the model file, so the byte order is a thing a reviewer reads rather than a
44//! thing this file decides on their behalf. That is the one fact about memory access that no
45//! amount of testing on one machine will catch.
46
47use std::collections::{BTreeMap, HashMap};
48
49use rucc_rules::{Error, Term, TermKind, parse_terms};
50
51/// The heads the solver already understands, and what SMT-LIB calls them.
52///
53/// The comparisons written as symbols are the signed ones. An unsigned comparison in a rule has
54/// to be written with the solver's own name for it, which is deliberate: a rule that means the
55/// unsigned one should have to say so rather than depend on which way this table happens to read.
56/// Both families are here under those names as well, so a rule that would rather be explicit
57/// about the signed one can be.
58const BUILTIN: [(&str, &str); 32] = [
59    ("=", "="),
60    ("and", "and"),
61    ("or", "or"),
62    ("not", "not"),
63    ("<", "bvslt"),
64    ("<=", "bvsle"),
65    (">", "bvsgt"),
66    (">=", "bvsge"),
67    ("bvslt", "bvslt"),
68    ("bvsle", "bvsle"),
69    ("bvsgt", "bvsgt"),
70    ("bvsge", "bvsge"),
71    ("bvult", "bvult"),
72    ("bvule", "bvule"),
73    ("bvugt", "bvugt"),
74    ("bvuge", "bvuge"),
75    ("bvadd", "bvadd"),
76    ("bvsub", "bvsub"),
77    ("bvmul", "bvmul"),
78    ("bvneg", "bvneg"),
79    ("bvnot", "bvnot"),
80    ("bvand", "bvand"),
81    ("bvor", "bvor"),
82    ("bvxor", "bvxor"),
83    ("bvshl", "bvshl"),
84    ("bvlshr", "bvlshr"),
85    ("bvashr", "bvashr"),
86    ("bvsdiv", "bvsdiv"),
87    ("bvudiv", "bvudiv"),
88    ("bvsrem", "bvsrem"),
89    ("bvurem", "bvurem"),
90    ("ite", "ite"),
91];
92
93/// The builtins that take a boolean somewhere, so their arguments are not all one width and
94/// there is nothing to check between them.
95const LOGICAL: [&str; 4] = ["and", "or", "not", "ite"];
96
97/// The heads that change width. Their first two arguments are widths rather than values, which
98/// is why they are written out here rather than sitting in [`BUILTIN`] with the rest: SMT-LIB
99/// spells them as indexed operators and the index is a number this has to work out.
100const CONVERSION: [&str; 3] = ["sign_extend", "zero_extend", "extract"];
101
102/// The heads that touch memory, which are not in [`BUILTIN`] because their arguments are not all
103/// the same sort and their results are not all the same sort either.
104const MEMORY: [&str; 3] = ["mem", "select", "store"];
105
106/// Putting bitvectors end to end, which is how a load of more than one byte is written. Not in
107/// [`BUILTIN`] because its arguments are one width and its result is their total.
108const CONCAT: &str = "concat";
109
110/// How wide an address is.
111///
112/// Every target `spec/12-abi-and-runtime.md` implements for 1.0 is sixty four bit, so this is a
113/// constant rather than something the model file says. When a thirty two bit target arrives it
114/// becomes something the model file says, and the rules that read memory will be the ones that
115/// notice.
116pub const ADDRESS_WIDTH: u32 = 64;
117
118/// How wide a byte is, which is the element of memory.
119pub const BYTE_WIDTH: u32 = 8;
120
121/// What the memory a rule starts from is called in the query.
122///
123/// A name no rule can bind, because a name in a rule comes out of a pattern and a pattern binds
124/// what the selector matched, which is registers and constants and never memory.
125pub const MEMORY_CONST: &str = "mem";
126
127/// What kind of thing a term computes.
128///
129/// Almost everything is a bitvector, and the exception is the whole point of this type: a rule
130/// with an effect relates one memory to another, and a memory is not a number however many bits
131/// one is willing to spend on it.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum Sort {
134    /// A bitvector this many bits wide.
135    Bits(u32),
136    /// The whole of memory, a map from an address to a byte.
137    Memory,
138}
139
140impl Sort {
141    /// How many bits wide it is, or nothing when it is not a bitvector at all.
142    #[must_use]
143    pub fn bits(self) -> Option<u32> {
144        match self {
145            Sort::Bits(width) => Some(width),
146            Sort::Memory => None,
147        }
148    }
149
150    /// What SMT-LIB calls it, at the widths this question is being asked at.
151    #[must_use]
152    pub fn write(self, widths: &Widths) -> String {
153        match self {
154            Sort::Bits(width) => format!("(_ BitVec {width})"),
155            Sort::Memory => {
156                format!("(Array (_ BitVec {}) (_ BitVec {}))", widths.address(), widths.byte())
157            }
158        }
159    }
160
161    /// How it reads in a message to somebody who has written a rule that does not fit together.
162    fn describe(self) -> String {
163        match self {
164            Sort::Bits(width) => format!("{width} bits wide"),
165            Sort::Memory => "the whole of memory".to_owned(),
166        }
167    }
168}
169
170/// What a rule works in when its opcode does not say. Every opcode in the IR does say, so this
171/// is what a hand written test rule gets rather than something the real rule set relies on.
172pub const DEFAULT_WIDTH: u32 = 64;
173
174/// How wide each thing in one rule is.
175///
176/// A rule is written at one width, the one its pattern's opcode names, and the terms inside it
177/// may say another: `(value.i64 x)` under an `add.i32` is a thirty two bit add of two sixty four
178/// bit registers, which is the shape every `sext`, `zext` and `trunc` in a lowering has. What a
179/// name stands at is fixed by the pattern, because the pattern is where a name is bound, and
180/// everywhere else reads it from here.
181///
182/// A bounded proof asks the same rule at a narrower width, and that scales every width in the
183/// rule by one ratio rather than flattening them all to one number. A rule that converts between
184/// widths still converts between widths when it is asked at eight bits, which it would not do if
185/// the narrow width were simply substituted everywhere.
186#[derive(Debug, Clone, Default)]
187pub struct Widths {
188    /// The width the rule is written in.
189    natural: u32,
190    /// The width it is being asked at, which is the same number unless this is a bounded proof.
191    asked: u32,
192    /// What each name the pattern binds stands at, already scaled.
193    at: BTreeMap<String, Sort>,
194}
195
196impl Widths {
197    /// The widths one rule's pattern fixes, at the width the rule is written in.
198    #[must_use]
199    pub fn of(pattern: &Term) -> Widths {
200        Widths::at(pattern, rule_width(pattern))
201    }
202
203    /// The same, scaled to a width somebody asked for. This is what a bounded proof is made of.
204    #[must_use]
205    pub fn at(pattern: &Term, asked: u32) -> Widths {
206        let natural = rule_width(pattern);
207        let mut widths = Widths { natural, asked, at: BTreeMap::new() };
208        widths.bind(pattern, asked);
209        widths
210    }
211
212    /// The width a term is at when nothing inside it says otherwise.
213    #[must_use]
214    pub fn width(&self) -> u32 {
215        self.asked
216    }
217
218    /// The width the rule is written in, which is the one it will run at.
219    #[must_use]
220    pub fn natural(&self) -> u32 {
221        self.natural
222    }
223
224    /// Every name the pattern binds and how wide it is, sorted.
225    ///
226    /// Sorted rather than in the order the pattern binds them, because the query is something a
227    /// test pins and a diff is easier to read than it is to regenerate.
228    pub fn names(&self) -> impl Iterator<Item = (&str, u32)> {
229        self.at.iter().filter_map(|(name, sort)| Some((name.as_str(), sort.bits()?)))
230    }
231
232    /// These widths and one more name, which is how the replacement's own meaning gets a width
233    /// once it has been substituted into the specification for `(result)`.
234    ///
235    /// A replacement that computes a memory is recorded as one, so that the specification which
236    /// reads it back is checked against a memory rather than against a number of bits nobody
237    /// meant.
238    #[must_use]
239    pub fn with(&self, name: &str, sort: Sort) -> Widths {
240        let mut out = self.clone();
241        out.at.insert(name.to_owned(), sort);
242        out
243    }
244
245    /// How wide an address is here, scaled like everything else.
246    #[must_use]
247    pub fn address(&self) -> u32 {
248        self.scale(ADDRESS_WIDTH)
249    }
250
251    /// How wide a byte is here, scaled like everything else.
252    ///
253    /// A bounded proof asks a rule in narrower bitvectors, and a byte narrows with them. It has
254    /// to: the bytes a load puts together have to add up to the value the load produces, and a
255    /// value that has been scaled and bytes that have not do not add up to anything.
256    #[must_use]
257    pub fn byte(&self) -> u32 {
258        self.scale(BYTE_WIDTH)
259    }
260
261    /// What a name stands for, when the pattern bound it.
262    fn of_name(&self, name: &str) -> Option<Sort> {
263        self.at.get(name).copied()
264    }
265
266    /// The width a head names, scaled.
267    fn suffix(&self, head: &str) -> Option<u32> {
268        declared(head).map(|width| self.scale(width))
269    }
270
271    /// A width, in the proportion the question is being asked at. Never nothing: a width that
272    /// scales to zero bits is a width the rule cannot be asked about at all.
273    fn scale(&self, width: u32) -> u32 {
274        if self.asked == self.natural || self.natural == 0 {
275            return width;
276        }
277        self.index(width).max(1)
278    }
279
280    /// A bit position, in the same proportion. Zero stays zero, which is what separates this
281    /// from [`Widths::scale`].
282    fn index(&self, position: u32) -> u32 {
283        if self.asked == self.natural || self.natural == 0 {
284            return position;
285        }
286        let scaled = u64::from(position) * u64::from(self.asked) / u64::from(self.natural);
287        u32::try_from(scaled).unwrap_or(position)
288    }
289
290    /// Walk the pattern and write down what each name it binds stands at.
291    fn bind(&mut self, term: &Term, context: u32) {
292        match &term.kind {
293            TermKind::Var(name) => {
294                self.at.insert(name.clone(), Sort::Bits(context));
295            }
296            TermKind::Int(_) => {}
297            TermKind::App { head, args } => {
298                let inner = self.suffix(head).unwrap_or(context);
299                for arg in args {
300                    self.bind(arg, inner);
301                }
302            }
303        }
304    }
305}
306
307/// The width a rule works in, taken from the suffix on its pattern's opcode.
308#[must_use]
309pub fn rule_width(pattern: &Term) -> u32 {
310    match &pattern.kind {
311        TermKind::App { head, .. } => declared(head).unwrap_or(DEFAULT_WIDTH),
312        _ => DEFAULT_WIDTH,
313    }
314}
315
316/// The width a head names, if it names one. `add.i32` does and `x64.lea` does not.
317fn declared(head: &str) -> Option<u32> {
318    head.rsplit_once('.')
319        .and_then(|(_, suffix)| suffix.strip_prefix('i'))
320        .and_then(|bits| bits.parse::<u32>().ok())
321}
322
323/// What one head means.
324#[derive(Debug, Clone)]
325struct Meaning {
326    /// The names the body is written in terms of.
327    params: Vec<String>,
328    /// What it computes.
329    body: Term,
330}
331
332/// Everything the rules are allowed to say, and what each of it means.
333#[derive(Debug, Default)]
334pub struct Model {
335    heads: HashMap<String, Meaning>,
336}
337
338impl Model {
339    /// Read a model from text.
340    ///
341    /// # Errors
342    ///
343    /// Anything that is not a well formed `(semantics (head params) body)` form, and any head
344    /// given a meaning twice.
345    pub fn read(path: &str, text: &str) -> Result<Model, Vec<Error>> {
346        let terms = parse_terms(path, text)?;
347        let mut model = Model::default();
348        let mut errors = Vec::new();
349
350        for term in terms {
351            let TermKind::App { head, args } = &term.kind else {
352                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
353                continue;
354            };
355            if head != "semantics" || args.len() != 2 {
356                errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
357                continue;
358            }
359            let TermKind::App { head: name, args: params } = &args[0].kind else {
360                errors.push(fail(path, &args[0], "expected a head and its parameters".to_owned()));
361                continue;
362            };
363            let mut names = Vec::new();
364            for param in params {
365                match &param.kind {
366                    TermKind::Var(name) => names.push(name.clone()),
367                    _ => errors.push(fail(path, param, "a parameter has to be a name".to_owned())),
368                }
369            }
370            if known(name) {
371                let said = format!("`{name}` is something the solver already knows");
372                errors.push(fail(path, &args[0], said));
373                continue;
374            }
375            let meaning = Meaning { params: names, body: args[1].clone() };
376            if model.heads.insert(name.clone(), meaning).is_some() {
377                let said = format!("`{name}` is given a meaning twice");
378                errors.push(fail(path, &args[0], said));
379            }
380        }
381
382        if errors.is_empty() { Ok(model) } else { Err(errors) }
383    }
384
385    /// Write one term out as SMT-LIB, expanding everything the model defines, and say how wide
386    /// what it computes is.
387    ///
388    /// # Errors
389    ///
390    /// A head that is neither a builtin nor in the model, since that is a term nobody has said
391    /// the meaning of, an application of the wrong number of arguments, and anything whose
392    /// widths do not fit together.
393    pub fn write(&self, path: &str, term: &Term, widths: &Widths) -> Result<(String, Sort), Error> {
394        self.write_at(path, term, widths.width(), widths, &HashMap::new())
395    }
396
397    /// Whether reading this term reaches memory, following every head the model defines.
398    ///
399    /// A rule that reads memory needs a solver told about arrays and a constant to stand for the
400    /// memory it starts from, and neither is worth putting in a query that does not. Nothing in a
401    /// rule says `(mem)` directly: a load says `load.i32`, and it is the model entry for that head
402    /// which reaches memory, so this expands what the model says rather than reading the surface.
403    #[must_use]
404    pub fn touches_memory(&self, term: &Term) -> bool {
405        match &term.kind {
406            TermKind::Var(_) | TermKind::Int(_) => false,
407            TermKind::App { head, args } => {
408                if MEMORY.contains(&head.as_str()) {
409                    return true;
410                }
411                if args.iter().any(|arg| self.touches_memory(arg)) {
412                    return true;
413                }
414                self.heads.get(head).is_some_and(|meaning| self.touches_memory(&meaning.body))
415            }
416        }
417    }
418
419    fn write_at(
420        &self,
421        path: &str,
422        term: &Term,
423        context: u32,
424        widths: &Widths,
425        bound: &HashMap<&str, (String, Sort)>,
426    ) -> Result<(String, Sort), Error> {
427        match &term.kind {
428            TermKind::Var(name) => match bound.get(name.as_str()) {
429                Some((already, sort)) => Ok((already.clone(), *sort)),
430                None => Ok((name.clone(), widths.of_name(name).unwrap_or(Sort::Bits(context)))),
431            },
432            TermKind::Int(value) => Ok((literal(*value, context), Sort::Bits(context))),
433            TermKind::App { head, args } => {
434                if CONVERSION.contains(&head.as_str()) {
435                    return self.convert(path, term, head, args, context, widths, bound);
436                }
437                if MEMORY.contains(&head.as_str()) {
438                    return self.reach(path, term, head, args, context, widths, bound);
439                }
440                if head == CONCAT {
441                    return self.join(path, term, args, context, widths, bound);
442                }
443                if let Some(name) = builtin(head) {
444                    return self.combine(path, term, head, name, args, context, widths, bound);
445                }
446                let own = widths.suffix(head).unwrap_or(context);
447                let mut written = Vec::with_capacity(args.len());
448                for arg in args {
449                    written.push(self.write_at(path, arg, own, widths, bound)?);
450                }
451                let Some(meaning) = self.heads.get(head) else {
452                    let said = format!("nothing in the model says what `{head}` means");
453                    return Err(fail(path, term, said));
454                };
455                if meaning.params.len() != written.len() {
456                    let said = format!(
457                        "`{head}` means something with {} arguments and this gives it {}",
458                        meaning.params.len(),
459                        written.len()
460                    );
461                    return Err(fail(path, term, said));
462                }
463                let inner: HashMap<&str, (String, Sort)> =
464                    meaning.params.iter().map(String::as_str).zip(written).collect();
465                let (text, sort) = self.write_at(path, &meaning.body, own, widths, &inner)?;
466                // An opcode that names a width has to mean something that wide. This is the
467                // model being held to what the rules say about it: `add.i32` over registers
468                // that are sixty four bits wide means an add of their low halves, and a model
469                // that leaves the truncation out says so here rather than in a proof that
470                // quietly asks the wrong question.
471                //
472                // A head that means a memory is the one exception, and it is not a hole. The
473                // width on `store.i32` is the width of what it wrote rather than of what it
474                // computes, and that width is checked all the same, by the extracts in the
475                // model entry having to come out of something that wide.
476                if let (Some(said), Some(width)) = (widths.suffix(head), sort.bits()) {
477                    if said != width {
478                        let told = format!(
479                            "`{head}` is written for {said} bits and means something {width} \
480                             bits wide"
481                        );
482                        return Err(fail(path, term, told));
483                    }
484                }
485                Ok((text, sort))
486            }
487        }
488    }
489
490    /// One of the heads the solver already knows, applied to arguments that all have to be the
491    /// same width unless a boolean is involved.
492    #[allow(clippy::too_many_arguments)]
493    fn combine(
494        &self,
495        path: &str,
496        term: &Term,
497        head: &str,
498        name: &str,
499        args: &[Term],
500        context: u32,
501        widths: &Widths,
502        bound: &HashMap<&str, (String, Sort)>,
503    ) -> Result<(String, Sort), Error> {
504        // A number has no width of its own and takes the width of what it sits beside. Every
505        // rule written before memory arrived had one width throughout, so this changed nothing
506        // for them, and it is what lets an offset added to an address in the model be as wide as
507        // the address rather than as wide as the value being loaded through it.
508        //
509        // Not under a head that takes a boolean. What a number sits beside there is a
510        // comparison, and a comparison has no width to lend: the one and the zero an `ite`
511        // chooses between are as wide as the term the `ite` is in, which is what `context` is.
512        let beside = if LOGICAL.contains(&head) {
513            context
514        } else {
515            self.beside(path, args, context, widths, bound)?
516        };
517        let mut written = Vec::with_capacity(args.len());
518        for arg in args {
519            let at = if matches!(arg.kind, TermKind::Int(_)) { beside } else { context };
520            written.push(self.write_at(path, arg, at, widths, bound)?);
521        }
522        let Some((_, first)) = written.first() else {
523            return Err(fail(path, term, format!("`{head}` needs arguments")));
524        };
525        let first = *first;
526        if !LOGICAL.contains(&head) {
527            if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
528                let said = format!(
529                    "`{head}` is given something {} and something {}, and those are not the \
530                     same kind of thing",
531                    first.describe(),
532                    other.describe()
533                );
534                return Err(fail(path, term, said));
535            }
536        }
537        // A comparison computes a boolean and its width is nobody's business, so saying it is
538        // as wide as what it compared costs nothing and keeps every term having an answer.
539        let sort = if head == "ite" && written.len() > 1 { written[1].1 } else { first };
540        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
541        Ok((format!("({name} {})", texts.join(" ")), sort))
542    }
543
544    /// The width the numbers among a head's arguments should take, which is the width of the
545    /// first argument that has one of its own. Nothing when they are all numbers, in which case
546    /// the surrounding width is as good an answer as there is.
547    fn beside(
548        &self,
549        path: &str,
550        args: &[Term],
551        context: u32,
552        widths: &Widths,
553        bound: &HashMap<&str, (String, Sort)>,
554    ) -> Result<u32, Error> {
555        if !args.iter().any(|arg| matches!(arg.kind, TermKind::Int(_))) {
556            return Ok(context);
557        }
558        let Some(sized) = args.iter().find(|arg| !matches!(arg.kind, TermKind::Int(_))) else {
559            return Ok(context);
560        };
561        let (_, sort) = self.write_at(path, sized, context, widths, bound)?;
562        Ok(sort.bits().unwrap_or(context))
563    }
564
565    /// One of the three heads that touch memory.
566    #[allow(clippy::too_many_arguments)]
567    fn reach(
568        &self,
569        path: &str,
570        term: &Term,
571        head: &str,
572        args: &[Term],
573        context: u32,
574        widths: &Widths,
575        bound: &HashMap<&str, (String, Sort)>,
576    ) -> Result<(String, Sort), Error> {
577        // The memory a rule starts from, which is one constant and takes no arguments. It is
578        // written `(mem)` for the reason `(result)` is: a head applied to nothing is still an
579        // application, because a bare name is a variable.
580        if head == "mem" {
581            if !args.is_empty() {
582                let said = "`mem` is the memory a rule starts from and takes nothing".to_owned();
583                return Err(fail(path, term, said));
584            }
585            return Ok((MEMORY_CONST.to_owned(), Sort::Memory));
586        }
587
588        let wanted = if head == "select" { 2 } else { 3 };
589        if args.len() != wanted {
590            let said =
591                format!("`{head}` takes {wanted} arguments and this gives it {}", args.len());
592            return Err(fail(path, term, said));
593        }
594        let mut written = Vec::with_capacity(args.len());
595        for arg in args {
596            let at = if matches!(arg.kind, TermKind::Int(_)) { widths.address() } else { context };
597            written.push(self.write_at(path, arg, at, widths, bound)?);
598        }
599        // The sorts of the three positions, which is the whole of what an array is: a memory, an
600        // address into it, and for a store the byte that goes there.
601        let expected = [Sort::Memory, Sort::Bits(widths.address()), Sort::Bits(widths.byte())];
602        for (at, (_, got)) in written.iter().enumerate() {
603            if *got != expected[at] {
604                let said = format!(
605                    "`{head}` takes something {} in position {at} and this is {}",
606                    expected[at].describe(),
607                    got.describe()
608                );
609                return Err(fail(path, term, said));
610            }
611        }
612        let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
613        let sort = if head == "select" { Sort::Bits(widths.byte()) } else { Sort::Memory };
614        Ok((format!("({head} {})", texts.join(" ")), sort))
615    }
616
617    /// Bitvectors end to end, which is as wide as all of them together.
618    ///
619    /// The first argument is the high end, which is how SMT-LIB reads it and is the opposite of
620    /// the order the bytes of a little endian load are at in memory. That is why a load in the
621    /// model file counts down.
622    fn join(
623        &self,
624        path: &str,
625        term: &Term,
626        args: &[Term],
627        context: u32,
628        widths: &Widths,
629        bound: &HashMap<&str, (String, Sort)>,
630    ) -> Result<(String, Sort), Error> {
631        if args.len() < 2 {
632            let said = format!("`concat` puts two or more things together and this gives it {}", {
633                args.len()
634            });
635            return Err(fail(path, term, said));
636        }
637        let mut total = 0;
638        let mut texts = Vec::with_capacity(args.len());
639        for arg in args {
640            let (text, sort) = self.write_at(path, arg, context, widths, bound)?;
641            let Some(width) = sort.bits() else {
642                let said = "`concat` puts bitvectors together and this is a memory".to_owned();
643                return Err(fail(path, arg, said));
644            };
645            total += width;
646            texts.push(text);
647        }
648        Ok((format!("(concat {})", texts.join(" ")), Sort::Bits(total)))
649    }
650
651    /// A conversion between widths, written as `spec/10-backend.md` writes it, with the widths
652    /// as arguments rather than inferred from anything.
653    #[allow(clippy::too_many_arguments)]
654    fn convert(
655        &self,
656        path: &str,
657        term: &Term,
658        head: &str,
659        args: &[Term],
660        context: u32,
661        widths: &Widths,
662        bound: &HashMap<&str, (String, Sort)>,
663    ) -> Result<(String, Sort), Error> {
664        if args.len() != 3 {
665            let said = format!("`{head}` takes two numbers and a value, and this gives it {}", {
666                args.len()
667            });
668            return Err(fail(path, term, said));
669        }
670        // Two numbers, and which two they are depends on the head: the bit positions an extract
671        // takes, and the widths an extension goes between.
672        let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
673
674        if head == "extract" {
675            let (high, low) = (first, second);
676            if high < low {
677                let said = format!("`extract` takes bits {high} down to {low}, which is none");
678                return Err(fail(path, term, said));
679            }
680            let width = widths.scale(high - low + 1);
681            let bottom = widths.index(low);
682            let top = bottom + width - 1;
683            let (text, sort) = self.write_at(path, &args[2], context, widths, bound)?;
684            let of = bits(path, head, &args[2], sort)?;
685            if top >= of {
686                let said = format!(
687                    "`extract` takes bits {top} down to {bottom} of something {of} bits wide"
688                );
689                return Err(fail(path, term, said));
690            }
691            return Ok((format!("((_ extract {top} {bottom}) {text})"), Sort::Bits(width)));
692        }
693
694        let (from, to) = (widths.scale(first), widths.scale(second));
695        if to < from {
696            let said = format!("`{head}` goes from {from} bits to {to}, which is narrower");
697            return Err(fail(path, term, said));
698        }
699        let (text, sort) = self.write_at(path, &args[2], from, widths, bound)?;
700        let of = bits(path, head, &args[2], sort)?;
701        if of != from {
702            let said =
703                format!("`{head}` goes from {from} bits and is given something {of} bits wide");
704            return Err(fail(path, term, said));
705        }
706        // Extending by nothing is written as nothing rather than as an extension by zero,
707        // because a bounded proof can scale two different widths onto the same one.
708        if to == from {
709            return Ok((text, Sort::Bits(to)));
710        }
711        Ok((format!("((_ {head} {}) {text})", to - from), Sort::Bits(to)))
712    }
713}
714
715/// What SMT-LIB calls this head, if it already knows it.
716fn builtin(head: &str) -> Option<&'static str> {
717    BUILTIN.iter().find(|(name, _)| *name == head).map(|(_, smt)| *smt)
718}
719
720/// Whether the solver already knows this head, and so whether the model may not redefine it.
721fn known(head: &str) -> bool {
722    builtin(head).is_some()
723        || CONVERSION.contains(&head)
724        || MEMORY.contains(&head)
725        || head == CONCAT
726}
727
728/// How wide something is, when it has to be a bitvector and the rule is wrong if it is not.
729fn bits(path: &str, head: &str, term: &Term, sort: Sort) -> Result<u32, Error> {
730    sort.bits().ok_or_else(|| {
731        let said = format!("`{head}` works on bitvectors and this is {}", sort.describe());
732        fail(path, term, said)
733    })
734}
735
736/// One of the numbers a conversion is written with.
737fn number(path: &str, head: &str, term: &Term) -> Result<u32, Error> {
738    match &term.kind {
739        TermKind::Int(value) => u32::try_from(*value).map_err(|_| {
740            let said = format!("`{head}` is given {value} where it needs a number of bits");
741            fail(path, term, said)
742        }),
743        _ => {
744            let said = format!("`{head}` says which widths it goes between, in numbers");
745            Err(fail(path, term, said))
746        }
747    }
748}
749
750/// A literal at the rule's width. Negative values are written as the bit pattern they are, since
751/// SMT-LIB has no sign on a bitvector literal.
752fn literal(value: i128, width: u32) -> String {
753    let wrapped =
754        if width >= 128 { value as u128 } else { (value as u128) & ((1u128 << width) - 1) };
755    format!("(_ bv{wrapped} {width})")
756}
757
758fn fail(path: &str, term: &Term, message: String) -> Error {
759    Error { path: path.to_owned(), line: term.line, column: term.column, message }
760}