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