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