logicaffeine_verify/ir.rs
1//! Verification IR (Intermediate Representation)
2//!
3//! A lightweight AST for Z3 verification that decouples from the main Logicaffeine AST.
4//! This avoids circular dependencies: logicaffeine depends on logicaffeine_verify,
5//! so logicaffeine_verify cannot depend on logicaffeine.
6//!
7//! ## Usage
8//!
9//! Build expressions using the [`VerifyExpr`] constructors:
10//!
11//! ```
12//! use logicaffeine_verify::{VerifyExpr, VerifyOp, VerifyType};
13//!
14//! // Build: x > 5 && y < 10
15//! let expr = VerifyExpr::and(
16//! VerifyExpr::gt(VerifyExpr::var("x"), VerifyExpr::int(5)),
17//! VerifyExpr::lt(VerifyExpr::var("y"), VerifyExpr::int(10)),
18//! );
19//! ```
20//!
21//! ## Encoding Strategy
22//!
23//! Complex types (modals, temporals, predicates) become uninterpreted functions.
24//! Z3 reasons about their structure without semantic understanding.
25//!
26//! For example, given `Possible(A) -> Possible(B)` and `Possible(A)`, Z3 deduces `Possible(B)`.
27
28use serde::{Serialize, Deserialize};
29
30/// Type declarations for verification variables.
31///
32/// Each type maps to a Z3 sort:
33///
34/// | VerifyType | Z3 Sort | Usage |
35/// |------------|---------|-------|
36/// | `Int` | `IntSort` | Numeric constraints, bounds checking |
37/// | `Bool` | `BoolSort` | Logical propositions |
38/// | `Object` | Uninterpreted | Entities (people, objects, propositions) |
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub enum VerifyType {
41 /// Integer type, maps to Z3 `IntSort`.
42 Int,
43 /// Boolean type, maps to Z3 `BoolSort`.
44 Bool,
45 /// Opaque object type for entities, maps to an uninterpreted sort.
46 Object,
47 /// Fixed-width bitvector, maps to Z3 `BitVecSort(n)`.
48 BitVector(u32),
49 /// Array type (index → element), maps to Z3 `ArraySort`.
50 Array(Box<VerifyType>, Box<VerifyType>),
51 /// Real number type, maps to Z3 `RealSort` (IEEE 1800-2023).
52 Real,
53}
54
55/// Binary operations in the verification IR.
56///
57/// Operations are grouped by category:
58/// - **Arithmetic**: `Add`, `Sub`, `Mul`, `Div` (Int × Int → Int)
59/// - **Comparison**: `Eq`, `Neq`, `Gt`, `Lt`, `Gte`, `Lte` (Int × Int → Bool)
60/// - **Logic**: `And`, `Or`, `Implies` (Bool × Bool → Bool)
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub enum VerifyOp {
63 // ---- Arithmetic (Int × Int → Int) ----
64
65 /// Addition.
66 Add,
67 /// Subtraction.
68 Sub,
69 /// Multiplication.
70 Mul,
71 /// Integer division (Euclidean, matching Z3's `div`).
72 Div,
73 /// Floor division (`a // b`) — the quotient rounded toward negative infinity, exactly.
74 /// Encoded as `to_int(to_real(a) / to_real(b))` (Z3's `to_int` is the floor function),
75 /// which is precise across every sign combination — unlike [`VerifyOp::Div`], whose
76 /// Euclidean rounding only coincides with floor when the divisor is positive.
77 FloorDiv,
78
79 // ---- Comparison (Int × Int → Bool) ----
80
81 /// Equality.
82 Eq,
83 /// Inequality.
84 Neq,
85 /// Greater than.
86 Gt,
87 /// Less than.
88 Lt,
89 /// Greater than or equal.
90 Gte,
91 /// Less than or equal.
92 Lte,
93
94 // ---- Logic (Bool × Bool → Bool) ----
95
96 /// Conjunction.
97 And,
98 /// Disjunction.
99 Or,
100 /// Material implication.
101 Implies,
102}
103
104/// Bitvector operations for hardware verification.
105///
106/// These map directly to Z3's bitvector theory operations.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum BitVecOp {
109 // ---- Bitwise ----
110 And,
111 Or,
112 Xor,
113 Not,
114
115 // ---- Shift ----
116 Shl,
117 Shr,
118 /// Arithmetic shift right (sign-extending).
119 AShr,
120
121 // ---- Arithmetic ----
122 Add,
123 Sub,
124 Mul,
125 /// Signed division, truncating toward zero (matches Rust `i64` `/`).
126 SDiv,
127 /// Signed remainder, sign following the dividend (matches Rust `i64` `%`).
128 SRem,
129
130 // ---- Comparison ----
131 /// Unsigned less than.
132 ULt,
133 /// Signed less than.
134 SLt,
135 /// Unsigned less than or equal.
136 ULe,
137 /// Signed less than or equal.
138 SLe,
139 /// Bitvector equality.
140 Eq,
141}
142
143/// Expression AST for verification.
144///
145/// This IR is designed to be easily encodable into Z3 ASTs.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub enum VerifyExpr {
148 /// Integer literal
149 Int(i64),
150
151 /// Boolean literal
152 Bool(bool),
153
154 /// Variable reference
155 Var(String),
156
157 /// Binary operation
158 Binary {
159 op: VerifyOp,
160 left: Box<VerifyExpr>,
161 right: Box<VerifyExpr>,
162 },
163
164 /// Logical negation
165 Not(Box<VerifyExpr>),
166
167 /// Universal quantifier: forall x: T. P(x)
168 ForAll {
169 vars: Vec<(String, VerifyType)>,
170 body: Box<VerifyExpr>,
171 },
172
173 /// Existential quantifier: exists x: T. P(x)
174 Exists {
175 vars: Vec<(String, VerifyType)>,
176 body: Box<VerifyExpr>,
177 },
178
179 /// Uninterpreted function application (the "catch-all")
180 ///
181 /// Used for predicates, modals, temporals, etc. that we can't
182 /// directly encode semantically. Z3 treats these as opaque functions
183 /// and reasons about them structurally.
184 ///
185 /// Examples:
186 /// - `Mortal(socrates)` -> `Apply { name: "Mortal", args: [Var("socrates")] }`
187 /// - `Possible(P)` -> `Apply { name: "Possible", args: [P] }`
188 Apply {
189 name: String,
190 args: Vec<VerifyExpr>,
191 },
192
193 /// Uninterpreted INT-valued function application: `Int^n → Int`.
194 ///
195 /// For function symbols in TERM position — `sum(a, b)` (the Link-lattice
196 /// join ⊕), `GovernmentOf(x)`, `has(x, y)` — where [`VerifyExpr::Apply`]
197 /// (which encodes as `Int^n → Bool`) would be ill-sorted.
198 ApplyInt {
199 name: String,
200 args: Vec<VerifyExpr>,
201 },
202
203 // ---- Bitvector operations (hardware verification) ----
204
205 /// Bitvector constant with explicit width.
206 BitVecConst { width: u32, value: u64 },
207
208 /// Bitvector binary operation.
209 BitVecBinary {
210 op: BitVecOp,
211 left: Box<VerifyExpr>,
212 right: Box<VerifyExpr>,
213 },
214
215 /// Bitvector bit extraction: operand\[high:low\].
216 BitVecExtract {
217 high: u32,
218 low: u32,
219 operand: Box<VerifyExpr>,
220 },
221
222 /// Bitvector concatenation.
223 BitVecConcat(Box<VerifyExpr>, Box<VerifyExpr>),
224
225 // ---- Temporal (BMC encoding) ----
226
227 /// Expression evaluated at a specific state (for BMC unrolling).
228 AtState {
229 state: Box<VerifyExpr>,
230 expr: Box<VerifyExpr>,
231 },
232
233 /// State transition relation: from → to.
234 Transition {
235 from: Box<VerifyExpr>,
236 to: Box<VerifyExpr>,
237 },
238
239 // ---- Array theory ----
240
241 /// Array select: array\[index\].
242 Select {
243 array: Box<VerifyExpr>,
244 index: Box<VerifyExpr>,
245 },
246
247 /// Array store: array\[index\] := value.
248 Store {
249 array: Box<VerifyExpr>,
250 index: Box<VerifyExpr>,
251 value: Box<VerifyExpr>,
252 },
253
254 // ---- Biconditional (equivalence checking) ----
255
256 /// Biconditional: left ↔ right. Used for Z3 equivalence queries.
257 Iff(Box<VerifyExpr>, Box<VerifyExpr>),
258}
259
260impl VerifyExpr {
261 /// Create a variable reference.
262 ///
263 /// # Examples
264 ///
265 /// ```
266 /// use logicaffeine_verify::VerifyExpr;
267 ///
268 /// let x = VerifyExpr::var("x");
269 /// let counter = VerifyExpr::var("counter");
270 /// ```
271 pub fn var(name: impl Into<String>) -> Self {
272 VerifyExpr::Var(name.into())
273 }
274
275 /// Create an integer literal.
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use logicaffeine_verify::VerifyExpr;
281 ///
282 /// let five = VerifyExpr::int(5);
283 /// let negative = VerifyExpr::int(-42);
284 /// ```
285 pub fn int(n: i64) -> Self {
286 VerifyExpr::Int(n)
287 }
288
289 /// Create a boolean literal.
290 ///
291 /// # Examples
292 ///
293 /// ```
294 /// use logicaffeine_verify::VerifyExpr;
295 ///
296 /// let truth = VerifyExpr::bool(true);
297 /// let falsity = VerifyExpr::bool(false);
298 /// ```
299 pub fn bool(b: bool) -> Self {
300 VerifyExpr::Bool(b)
301 }
302
303 /// Create a binary operation.
304 ///
305 /// For common operations, prefer the convenience methods like [`eq`](Self::eq),
306 /// [`gt`](Self::gt), [`and`](Self::and), etc.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use logicaffeine_verify::{VerifyExpr, VerifyOp};
312 ///
313 /// // x + y
314 /// let sum = VerifyExpr::binary(
315 /// VerifyOp::Add,
316 /// VerifyExpr::var("x"),
317 /// VerifyExpr::var("y"),
318 /// );
319 /// ```
320 pub fn binary(op: VerifyOp, left: VerifyExpr, right: VerifyExpr) -> Self {
321 VerifyExpr::Binary {
322 op,
323 left: Box::new(left),
324 right: Box::new(right),
325 }
326 }
327
328 /// Create a negation.
329 ///
330 /// # Examples
331 ///
332 /// ```
333 /// use logicaffeine_verify::VerifyExpr;
334 ///
335 /// // ¬p
336 /// let not_p = VerifyExpr::not(VerifyExpr::var("p"));
337 ///
338 /// // ¬(x > 5)
339 /// let not_gt = VerifyExpr::not(
340 /// VerifyExpr::gt(VerifyExpr::var("x"), VerifyExpr::int(5))
341 /// );
342 /// ```
343 pub fn not(expr: VerifyExpr) -> Self {
344 VerifyExpr::Not(Box::new(expr))
345 }
346
347 /// Create an uninterpreted function application.
348 ///
349 /// Use this for predicates, modals, temporals, and other constructs
350 /// that cannot be directly encoded semantically. Z3 treats these as
351 /// opaque functions and reasons about them structurally.
352 ///
353 /// # Examples
354 ///
355 /// ```
356 /// use logicaffeine_verify::VerifyExpr;
357 ///
358 /// // Mortal(socrates)
359 /// let mortal = VerifyExpr::apply("Mortal", vec![VerifyExpr::var("socrates")]);
360 ///
361 /// // Possible(P) for modal logic
362 /// let possible_p = VerifyExpr::apply("Possible", vec![VerifyExpr::var("P")]);
363 ///
364 /// // Before(e1, e2) for temporal relations
365 /// let before = VerifyExpr::apply("Before", vec![
366 /// VerifyExpr::var("e1"),
367 /// VerifyExpr::var("e2"),
368 /// ]);
369 /// ```
370 pub fn apply(name: impl Into<String>, args: Vec<VerifyExpr>) -> Self {
371 VerifyExpr::Apply {
372 name: name.into(),
373 args,
374 }
375 }
376
377 /// Create an uninterpreted INT-valued function application (`Int^n → Int`)
378 /// for function symbols in term position, e.g. the lattice join
379 /// `sum(a, b)`.
380 pub fn apply_int(name: impl Into<String>, args: Vec<VerifyExpr>) -> Self {
381 VerifyExpr::ApplyInt {
382 name: name.into(),
383 args,
384 }
385 }
386
387 /// Create a universal quantifier.
388 ///
389 /// # Examples
390 ///
391 /// ```
392 /// use logicaffeine_verify::{VerifyExpr, VerifyType};
393 ///
394 /// // ∀x: Object. Mortal(x) → Human(x)
395 /// let all_mortals_are_human = VerifyExpr::forall(
396 /// vec![("x".to_string(), VerifyType::Object)],
397 /// VerifyExpr::implies(
398 /// VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]),
399 /// VerifyExpr::apply("Human", vec![VerifyExpr::var("x")]),
400 /// ),
401 /// );
402 /// ```
403 pub fn forall(vars: Vec<(String, VerifyType)>, body: VerifyExpr) -> Self {
404 VerifyExpr::ForAll {
405 vars,
406 body: Box::new(body),
407 }
408 }
409
410 /// Create an existential quantifier.
411 ///
412 /// # Examples
413 ///
414 /// ```
415 /// use logicaffeine_verify::{VerifyExpr, VerifyType};
416 ///
417 /// // ∃x: Object. Mortal(x)
418 /// let something_is_mortal = VerifyExpr::exists(
419 /// vec![("x".to_string(), VerifyType::Object)],
420 /// VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]),
421 /// );
422 /// ```
423 pub fn exists(vars: Vec<(String, VerifyType)>, body: VerifyExpr) -> Self {
424 VerifyExpr::Exists {
425 vars,
426 body: Box::new(body),
427 }
428 }
429
430 // ---- Convenience methods for common operations ----
431
432 /// Equality: `left == right`.
433 ///
434 /// # Examples
435 ///
436 /// ```
437 /// use logicaffeine_verify::VerifyExpr;
438 ///
439 /// let x_equals_10 = VerifyExpr::eq(VerifyExpr::var("x"), VerifyExpr::int(10));
440 /// ```
441 pub fn eq(left: VerifyExpr, right: VerifyExpr) -> Self {
442 Self::binary(VerifyOp::Eq, left, right)
443 }
444
445 /// Greater than: `left > right`.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// use logicaffeine_verify::VerifyExpr;
451 ///
452 /// let x_gt_5 = VerifyExpr::gt(VerifyExpr::var("x"), VerifyExpr::int(5));
453 /// ```
454 pub fn gt(left: VerifyExpr, right: VerifyExpr) -> Self {
455 Self::binary(VerifyOp::Gt, left, right)
456 }
457
458 /// Less than: `left < right`.
459 ///
460 /// # Examples
461 ///
462 /// ```
463 /// use logicaffeine_verify::VerifyExpr;
464 ///
465 /// let x_lt_100 = VerifyExpr::lt(VerifyExpr::var("x"), VerifyExpr::int(100));
466 /// ```
467 pub fn lt(left: VerifyExpr, right: VerifyExpr) -> Self {
468 Self::binary(VerifyOp::Lt, left, right)
469 }
470
471 /// Greater than or equal: `left >= right`.
472 ///
473 /// # Examples
474 ///
475 /// ```
476 /// use logicaffeine_verify::VerifyExpr;
477 ///
478 /// let x_gte_0 = VerifyExpr::gte(VerifyExpr::var("x"), VerifyExpr::int(0));
479 /// ```
480 pub fn gte(left: VerifyExpr, right: VerifyExpr) -> Self {
481 Self::binary(VerifyOp::Gte, left, right)
482 }
483
484 /// Less than or equal: `left <= right`.
485 ///
486 /// # Examples
487 ///
488 /// ```
489 /// use logicaffeine_verify::VerifyExpr;
490 ///
491 /// let x_lte_max = VerifyExpr::lte(VerifyExpr::var("x"), VerifyExpr::var("max"));
492 /// ```
493 pub fn lte(left: VerifyExpr, right: VerifyExpr) -> Self {
494 Self::binary(VerifyOp::Lte, left, right)
495 }
496
497 /// Inequality: `left != right`.
498 ///
499 /// # Examples
500 ///
501 /// ```
502 /// use logicaffeine_verify::VerifyExpr;
503 ///
504 /// let x_neq_0 = VerifyExpr::neq(VerifyExpr::var("x"), VerifyExpr::int(0));
505 /// ```
506 pub fn neq(left: VerifyExpr, right: VerifyExpr) -> Self {
507 Self::binary(VerifyOp::Neq, left, right)
508 }
509
510 /// Conjunction: `left && right`.
511 ///
512 /// # Examples
513 ///
514 /// ```
515 /// use logicaffeine_verify::VerifyExpr;
516 ///
517 /// // x > 0 && x < 100
518 /// let in_range = VerifyExpr::and(
519 /// VerifyExpr::gt(VerifyExpr::var("x"), VerifyExpr::int(0)),
520 /// VerifyExpr::lt(VerifyExpr::var("x"), VerifyExpr::int(100)),
521 /// );
522 /// ```
523 pub fn and(left: VerifyExpr, right: VerifyExpr) -> Self {
524 Self::binary(VerifyOp::And, left, right)
525 }
526
527 /// Disjunction: `left || right`.
528 ///
529 /// # Examples
530 ///
531 /// ```
532 /// use logicaffeine_verify::VerifyExpr;
533 ///
534 /// // x < 0 || x > 100
535 /// let out_of_range = VerifyExpr::or(
536 /// VerifyExpr::lt(VerifyExpr::var("x"), VerifyExpr::int(0)),
537 /// VerifyExpr::gt(VerifyExpr::var("x"), VerifyExpr::int(100)),
538 /// );
539 /// ```
540 pub fn or(left: VerifyExpr, right: VerifyExpr) -> Self {
541 Self::binary(VerifyOp::Or, left, right)
542 }
543
544 /// Material implication: `left → right`.
545 ///
546 /// # Examples
547 ///
548 /// ```
549 /// use logicaffeine_verify::VerifyExpr;
550 ///
551 /// // Mortal(x) → Human(x)
552 /// let mortals_are_human = VerifyExpr::implies(
553 /// VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]),
554 /// VerifyExpr::apply("Human", vec![VerifyExpr::var("x")]),
555 /// );
556 /// ```
557 pub fn implies(left: VerifyExpr, right: VerifyExpr) -> Self {
558 Self::binary(VerifyOp::Implies, left, right)
559 }
560
561 // ---- Bitvector convenience constructors ----
562
563 /// Create a bitvector constant.
564 pub fn bv_const(width: u32, value: u64) -> Self {
565 VerifyExpr::BitVecConst { width, value }
566 }
567
568 /// Create a bitvector binary operation.
569 pub fn bv_binary(op: BitVecOp, left: VerifyExpr, right: VerifyExpr) -> Self {
570 VerifyExpr::BitVecBinary {
571 op,
572 left: Box::new(left),
573 right: Box::new(right),
574 }
575 }
576
577 /// Biconditional: `left ↔ right`.
578 pub fn iff(left: VerifyExpr, right: VerifyExpr) -> Self {
579 VerifyExpr::Iff(Box::new(left), Box::new(right))
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn test_verify_expr_construction() {
589 // Test that we can construct expressions
590 let x = VerifyExpr::var("x");
591 let five = VerifyExpr::int(5);
592 let ten = VerifyExpr::int(10);
593
594 // x > 5
595 let gt = VerifyExpr::gt(x.clone(), five);
596 assert!(matches!(gt, VerifyExpr::Binary { op: VerifyOp::Gt, .. }));
597
598 // x == 10
599 let eq = VerifyExpr::eq(x.clone(), ten);
600 assert!(matches!(eq, VerifyExpr::Binary { op: VerifyOp::Eq, .. }));
601 }
602
603 #[test]
604 fn test_uninterpreted_function() {
605 // Mortal(x)
606 let mortal_x = VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]);
607 assert!(matches!(mortal_x, VerifyExpr::Apply { name, args } if name == "Mortal" && args.len() == 1));
608 }
609
610 #[test]
611 fn test_implication() {
612 // Mortal(x) -> Human(x)
613 let mortal = VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]);
614 let human = VerifyExpr::apply("Human", vec![VerifyExpr::var("x")]);
615 let impl_expr = VerifyExpr::implies(mortal, human);
616
617 assert!(matches!(impl_expr, VerifyExpr::Binary { op: VerifyOp::Implies, .. }));
618 }
619
620 #[test]
621 fn test_quantifier() {
622 // forall x: Object. Mortal(x) -> Human(x)
623 let body = VerifyExpr::implies(
624 VerifyExpr::apply("Mortal", vec![VerifyExpr::var("x")]),
625 VerifyExpr::apply("Human", vec![VerifyExpr::var("x")]),
626 );
627 let forall = VerifyExpr::forall(
628 vec![("x".to_string(), VerifyType::Object)],
629 body,
630 );
631
632 assert!(matches!(forall, VerifyExpr::ForAll { vars, .. } if vars.len() == 1));
633 }
634}