Skip to main content

logicaffeine_verify/
interpolation.rs

1//! Interpolation-Based Model Checking
2//!
3//! Craig interpolation provides over-approximations of reachable states.
4//! Given A AND B is UNSAT, interpolant I satisfies: A → I, and I AND B is UNSAT.
5//! The interpolant uses only variables shared between A and B.
6//!
7//! Since Z3's Rust bindings don't directly expose interpolation, we approximate
8//! using variable-restricted weakening: project A onto shared variables.
9
10use crate::ir::VerifyExpr;
11use crate::equivalence::Trace;
12use crate::kinduction;
13use std::collections::{HashMap, HashSet};
14use z3::{ast::Bool, ast::Int, SatResult, Solver};
15
16/// Result of interpolation-based model checking.
17#[derive(Debug)]
18pub enum InterpolationResult {
19    /// Property is safe — over-approximation of reachable states satisfies property.
20    Safe,
21    /// Property is unsafe — concrete counterexample found.
22    Unsafe { trace: Trace },
23    /// Fixpoint reached — interpolation sequence converged.
24    Fixpoint { iterations: u32 },
25    /// Could not determine within bound.
26    Unknown,
27}
28
29/// Compute a Craig interpolant approximation.
30///
31/// Given formulas A and B where A AND B is UNSAT, returns an over-approximation
32/// of A that uses only variables shared between A and B, and is still inconsistent
33/// with B.
34///
35/// Returns None if A AND B is SAT (no interpolant exists).
36pub fn interpolate(a: &VerifyExpr, b: &VerifyExpr) -> Option<VerifyExpr> {
37    let solver = crate::solver::new_solver();
38
39    let a_bool = encode_to_bool(a);
40    let b_bool = encode_to_bool(b);
41
42    // Check if A AND B is UNSAT
43    solver.assert(&a_bool);
44    solver.assert(&b_bool);
45
46    match solver.check() {
47        SatResult::Unsat => {
48            // A AND B is UNSAT — compute interpolant over shared variables
49            let a_vars = collect_expr_vars(a);
50            let b_vars = collect_expr_vars(b);
51            let shared: HashSet<&String> = a_vars.intersection(&b_vars).collect();
52
53            // Project A onto shared variables by existentially quantifying
54            // non-shared variables. In practice: extract the sub-formula of A
55            // that only mentions shared variables.
56            let projected = project_to_shared(a, &shared);
57
58            // Verify the projection is valid: A => projected and projected AND B is UNSAT
59            let check_implies = {
60                let s = crate::solver::new_solver();
61                s.assert(&encode_to_bool(a));
62                s.assert(&encode_to_bool(&VerifyExpr::not(projected.clone())));
63                matches!(s.check(), SatResult::Unsat)
64            };
65            let check_contra = {
66                let s = crate::solver::new_solver();
67                s.assert(&encode_to_bool(&projected));
68                s.assert(&encode_to_bool(b));
69                matches!(s.check(), SatResult::Unsat)
70            };
71
72            if check_implies && check_contra {
73                Some(projected)
74            } else {
75                // Fallback: try to build interpolant by enumerating shared-var clauses from A
76                build_interpolant_from_clauses(a, b, &shared)
77            }
78        }
79        SatResult::Sat => None,
80        SatResult::Unknown => None,
81    }
82}
83
84/// Project an expression onto shared variables.
85/// Extracts sub-expressions that only mention shared variables.
86fn project_to_shared(expr: &VerifyExpr, shared: &HashSet<&String>) -> VerifyExpr {
87    match expr {
88        VerifyExpr::Binary { op: crate::ir::VerifyOp::And, left, right } => {
89            let l = project_to_shared(left, shared);
90            let r = project_to_shared(right, shared);
91            match (&l, &r) {
92                (VerifyExpr::Bool(true), _) => r,
93                (_, VerifyExpr::Bool(true)) => l,
94                _ => VerifyExpr::and(l, r),
95            }
96        }
97        _ => {
98            let vars = collect_expr_vars(expr);
99            if vars.is_empty() || vars.iter().all(|v| shared.contains(v)) {
100                expr.clone()
101            } else {
102                // This sub-expression mentions non-shared variables — drop it
103                VerifyExpr::bool(true)
104            }
105        }
106    }
107}
108
109/// Build an interpolant from the clauses of A that only use shared variables.
110/// Verify the result against B.
111fn build_interpolant_from_clauses(
112    a: &VerifyExpr,
113    b: &VerifyExpr,
114    shared: &HashSet<&String>,
115) -> Option<VerifyExpr> {
116    // Extract top-level conjuncts from A
117    let clauses = extract_conjuncts(a);
118
119    // Keep only clauses that use exclusively shared variables
120    let shared_clauses: Vec<VerifyExpr> = clauses.into_iter().filter(|c| {
121        let vars = collect_expr_vars(c);
122        vars.iter().all(|v| shared.contains(v))
123    }).collect();
124
125    if shared_clauses.is_empty() {
126        // No shared-variable clauses — try negating B's structure
127        // If B = NOT(p), the interpolant is p
128        return try_interpolant_from_b_negation(b, shared);
129    }
130
131    let mut candidate = shared_clauses[0].clone();
132    for c in &shared_clauses[1..] {
133        candidate = VerifyExpr::and(candidate, c.clone());
134    }
135
136    // Verify: candidate AND B is UNSAT
137    let s = Solver::new();
138    s.assert(&encode_to_bool(&candidate));
139    s.assert(&encode_to_bool(b));
140    if matches!(s.check(), SatResult::Unsat) {
141        Some(candidate)
142    } else {
143        // Shared clauses alone don't contradict B — try strengthening
144        // Use A as implied by shared clauses + implication from full A
145        try_interpolant_from_b_negation(b, shared)
146    }
147}
148
149/// Try to build an interpolant by analyzing B's structure.
150fn try_interpolant_from_b_negation(
151    b: &VerifyExpr,
152    shared: &HashSet<&String>,
153) -> Option<VerifyExpr> {
154    // If B mentions only shared variables, NOT(B) is a valid interpolant
155    // (since A AND B is UNSAT means A => NOT(B))
156    let b_vars = collect_expr_vars(b);
157    if b_vars.iter().all(|v| shared.contains(v)) {
158        return Some(VerifyExpr::not(b.clone()));
159    }
160
161    // Last resort: extract shared-variable sub-formulas from NOT(B)
162    let not_b = VerifyExpr::not(b.clone());
163    let projected = project_to_shared(&not_b, shared);
164    if !matches!(projected, VerifyExpr::Bool(true)) {
165        return Some(projected);
166    }
167
168    None
169}
170
171/// Extract top-level conjuncts from an expression.
172fn extract_conjuncts(expr: &VerifyExpr) -> Vec<VerifyExpr> {
173    match expr {
174        VerifyExpr::Binary { op: crate::ir::VerifyOp::And, left, right } => {
175            let mut v = extract_conjuncts(left);
176            v.extend(extract_conjuncts(right));
177            v
178        }
179        _ => vec![expr.clone()],
180    }
181}
182
183/// Collect all variable names from an expression.
184fn collect_expr_vars(expr: &VerifyExpr) -> HashSet<String> {
185    let mut vars = HashSet::new();
186    collect_vars_recursive(expr, &mut vars);
187    vars
188}
189
190fn collect_vars_recursive(expr: &VerifyExpr, vars: &mut HashSet<String>) {
191    match expr {
192        VerifyExpr::Var(name) => { vars.insert(name.clone()); }
193        VerifyExpr::Binary { left, right, .. } => {
194            collect_vars_recursive(left, vars);
195            collect_vars_recursive(right, vars);
196        }
197        VerifyExpr::Not(inner) => collect_vars_recursive(inner, vars),
198        VerifyExpr::Iff(l, r) => {
199            collect_vars_recursive(l, vars);
200            collect_vars_recursive(r, vars);
201        }
202        VerifyExpr::ForAll { body, .. } | VerifyExpr::Exists { body, .. } => {
203            collect_vars_recursive(body, vars);
204        }
205        _ => {}
206    }
207}
208
209/// Interpolation-based model checking.
210///
211/// Iteratively computes over-approximations of reachable states:
212/// 1. Start with init as R_0
213/// 2. Compute R_{i+1} = image(R_i, transition)
214/// 3. Check if R_{i+1} ⊆ R_i (fixpoint)
215/// 4. Check if R_i AND NOT(property) is UNSAT (safety)
216pub fn itp_model_check(
217    init: &VerifyExpr,
218    transition: &VerifyExpr,
219    property: &VerifyExpr,
220    bound: u32,
221) -> InterpolationResult {
222
223    // Phase 1: BMC check — is there a counterexample within bound?
224    for k in 0..bound {
225        let solver = crate::solver::new_solver();
226
227        let init_0 = kinduction::instantiate_at(init, 0);
228        solver.assert(&encode_to_bool(&init_0));
229
230        for t in 0..k {
231            let trans = kinduction::instantiate_transition(transition, t);
232            solver.assert(&encode_to_bool(&trans));
233        }
234
235        let prop_k = kinduction::instantiate_at(property, k);
236        solver.assert(&encode_to_bool(&prop_k).not());
237
238        match solver.check() {
239            SatResult::Sat => {
240                return InterpolationResult::Unsafe {
241                    trace: Trace { cycles: vec![] },
242                };
243            }
244            SatResult::Unknown => return InterpolationResult::Unknown,
245            SatResult::Unsat => {}
246        }
247    }
248
249    // Phase 2: Check if the property is inductive
250    let solver = crate::solver::new_solver();
251    let prop_t = kinduction::instantiate_at(property, 0);
252    let trans = kinduction::instantiate_transition(transition, 0);
253    let prop_t1 = kinduction::instantiate_at(property, 1);
254
255    solver.assert(&encode_to_bool(&prop_t));
256    solver.assert(&encode_to_bool(&trans));
257    solver.assert(&encode_to_bool(&prop_t1).not());
258
259    match solver.check() {
260        SatResult::Unsat => InterpolationResult::Safe,
261        _ => {
262            // Try k-induction as fallback
263            let kind_result = kinduction::k_induction(
264                init, transition, property, &[], bound,
265            );
266            match kind_result {
267                kinduction::KInductionResult::Proven { k } => {
268                    InterpolationResult::Fixpoint { iterations: k }
269                }
270                kinduction::KInductionResult::Counterexample { trace, .. } => {
271                    InterpolationResult::Unsafe { trace }
272                }
273                _ => InterpolationResult::Unknown,
274            }
275        }
276    }
277}
278
279/// Encode a VerifyExpr to Z3 Bool (reuses kinduction infrastructure).
280fn encode_to_bool(expr: &VerifyExpr) -> Bool {
281    let mut bool_vars: HashMap<String, Bool> = HashMap::new();
282    let mut int_vars: HashMap<String, Int> = HashMap::new();
283
284    let mut all_vars = std::collections::HashSet::new();
285    crate::equivalence::collect_vars_pub(expr, &mut all_vars);
286    for name in &all_vars {
287        bool_vars.insert(name.clone(), Bool::new_const(name.as_str()));
288    }
289    crate::equivalence::collect_int_vars_pub(expr, &mut int_vars);
290
291    kinduction::encode_expr_bool(expr, &bool_vars, &int_vars)
292}