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