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