Skip to main content

logicaffeine_verify/
abstraction.rs

1//! Predicate Abstraction with CEGAR Refinement
2//!
3//! Predicate abstraction reduces an infinite-state system to a finite-state
4//! boolean system over abstract predicate variables. Each concrete predicate
5//! (e.g., `x >= 0`) becomes an abstract boolean. The abstract transition encodes
6//! how predicate truth values evolve under the concrete transition relation.
7//!
8//! CEGAR (Counterexample-Guided Abstraction Refinement) iterates:
9//! 1. Abstract: build finite-state model from current predicates
10//! 2. Verify: check property on abstract model (k-induction)
11//! 3. If safe: done
12//! 4. If counterexample: check if realizable on concrete model
13//! 5. If real: return Unsafe
14//! 6. If spurious: extract new predicates from the infeasibility proof, refine
15
16use crate::ir::{VerifyExpr, VerifyOp};
17use crate::equivalence::Trace;
18use crate::kinduction;
19use std::collections::{HashMap, HashSet};
20use z3::{ast::Bool, SatResult};
21
22#[derive(Debug, Clone)]
23pub struct AbstractModel {
24    pub predicates: Vec<VerifyExpr>,
25    pub abstract_init: VerifyExpr,
26    pub abstract_transition: VerifyExpr,
27}
28
29#[derive(Debug)]
30pub enum AbstractionResult {
31    Safe,
32    Unsafe { concrete_trace: Trace },
33    SpuriousRefined { new_predicates: Vec<VerifyExpr> },
34    Unknown,
35}
36
37/// Create an abstract model from initial state and predicates (legacy 2-arg API).
38///
39/// Without the transition relation, the abstract transition cannot be computed
40/// properly, so it defaults to the conjunction of predicates as a conservative
41/// overapproximation.
42pub fn abstract_model(
43    init: &VerifyExpr,
44    predicates: &[VerifyExpr],
45) -> AbstractModel {
46    let abstract_init = if predicates.is_empty() {
47        init.clone()
48    } else {
49        let mut abs = init.clone();
50        for pred in predicates {
51            abs = VerifyExpr::and(abs, pred.clone());
52        }
53        abs
54    };
55
56    AbstractModel {
57        predicates: predicates.to_vec(),
58        abstract_init,
59        abstract_transition: VerifyExpr::bool(true),
60    }
61}
62
63/// Create an abstract model with Cartesian predicate abstraction.
64///
65/// Given concrete init, transition, and a set of predicates, this computes:
66///
67/// - `abstract_init`: init AND conjunction of all predicates
68/// - `abstract_transition`: concrete transition AND inductive predicate implications
69///
70/// For each predicate p_i, we check via Z3 whether p_i is inductive relative to
71/// the transition: `(p_i@t AND T(t, t1)) => p_i@t1`. If valid (UNSAT negation),
72/// the implication is included in the abstract transition. If not valid, the
73/// predicate may take any value in the next state (overapproximation).
74///
75/// This is Cartesian predicate abstraction: each predicate is checked independently.
76pub fn abstract_model_full(
77    init: &VerifyExpr,
78    transition: &VerifyExpr,
79    predicates: &[VerifyExpr],
80) -> AbstractModel {
81    if predicates.is_empty() {
82        return AbstractModel {
83            predicates: vec![],
84            abstract_init: init.clone(),
85            abstract_transition: transition.clone(),
86        };
87    }
88
89    // Abstract init: init AND each predicate
90    let mut abstract_init = init.clone();
91    for pred in predicates {
92        abstract_init = VerifyExpr::and(abstract_init, pred.clone());
93    }
94
95    // Abstract transition: start with the concrete transition, then add
96    // inductive predicate implications.
97    let mut abstract_trans_parts: Vec<VerifyExpr> = Vec::new();
98    abstract_trans_parts.push(transition.clone());
99
100
101    for pred in predicates {
102        let pred_next = replace_t_with_t1(pred);
103
104        // Check inductiveness: is (pred@t AND T) => pred@t1 valid?
105        // Equivalently: is (pred@t AND T AND NOT pred@t1) unsatisfiable?
106        let check = VerifyExpr::and(
107            pred.clone(),
108            VerifyExpr::and(
109                transition.clone(),
110                VerifyExpr::not(pred_next.clone()),
111            ),
112        );
113
114        // Instantiate at step 0 for the Z3 check
115        let check_inst = kinduction::instantiate_transition(
116            &kinduction::instantiate_at(&check, 0),
117            0,
118        );
119
120        let is_inductive = {
121            let solver = crate::solver::new_solver();
122            let encoded = encode_bool(&check_inst);
123            solver.assert(&encoded);
124            matches!(solver.check(), SatResult::Unsat)
125        };
126
127        if is_inductive {
128            // Predicate is preserved by transition: include as hard constraint
129            let implication = VerifyExpr::implies(
130                VerifyExpr::and(pred.clone(), transition.clone()),
131                pred_next,
132            );
133            abstract_trans_parts.push(implication);
134        } else {
135            // Predicate is NOT inductive: include a weaker constraint.
136            // Record that this predicate participates in the abstraction
137            // by including it as a disjunction: pred@t1 OR NOT pred@t1
138            // (i.e., the predicate can take any value, but we record its presence).
139            //
140            // We use a softer form: (pred@t AND transition) => (pred@t1 OR NOT pred@t)
141            // This is always valid and documents the predicate's participation
142            // without constraining the abstract system.
143            let soft = VerifyExpr::implies(
144                pred.clone(),
145                VerifyExpr::or(pred_next.clone(), VerifyExpr::not(pred_next)),
146            );
147            abstract_trans_parts.push(soft);
148        }
149    }
150
151    let mut abstract_transition = abstract_trans_parts[0].clone();
152    for part in &abstract_trans_parts[1..] {
153        abstract_transition = VerifyExpr::and(abstract_transition, part.clone());
154    }
155
156    AbstractModel {
157        predicates: predicates.to_vec(),
158        abstract_init,
159        abstract_transition,
160    }
161}
162
163/// Replace @t with @t1 in all variable names of an expression.
164fn replace_t_with_t1(expr: &VerifyExpr) -> VerifyExpr {
165    match expr {
166        VerifyExpr::Var(name) => {
167            if name.ends_with("@t") {
168                let base = &name[..name.len() - 2];
169                VerifyExpr::Var(format!("{}@t1", base))
170            } else {
171                VerifyExpr::Var(name.clone())
172            }
173        }
174        VerifyExpr::Binary { op, left, right } => VerifyExpr::binary(
175            *op,
176            replace_t_with_t1(left),
177            replace_t_with_t1(right),
178        ),
179        VerifyExpr::Not(inner) => VerifyExpr::not(replace_t_with_t1(inner)),
180        VerifyExpr::Bool(b) => VerifyExpr::Bool(*b),
181        VerifyExpr::Int(n) => VerifyExpr::Int(*n),
182        VerifyExpr::Iff(l, r) => VerifyExpr::iff(replace_t_with_t1(l), replace_t_with_t1(r)),
183        VerifyExpr::ForAll { vars, body } => VerifyExpr::forall(
184            vars.clone(),
185            replace_t_with_t1(body),
186        ),
187        VerifyExpr::Exists { vars, body } => VerifyExpr::exists(
188            vars.clone(),
189            replace_t_with_t1(body),
190        ),
191        VerifyExpr::ApplyInt { name, args } => VerifyExpr::apply_int(
192            name.clone(),
193            args.iter().map(|a| replace_t_with_t1(a)).collect(),
194        ),
195        VerifyExpr::Apply { name, args } => VerifyExpr::apply(
196            name.clone(),
197            args.iter().map(|a| replace_t_with_t1(a)).collect(),
198        ),
199        VerifyExpr::BitVecConst { width, value } => VerifyExpr::bv_const(*width, *value),
200        VerifyExpr::BitVecBinary { op, left, right } => VerifyExpr::bv_binary(
201            *op,
202            replace_t_with_t1(left),
203            replace_t_with_t1(right),
204        ),
205        VerifyExpr::BitVecExtract { high, low, operand } => VerifyExpr::BitVecExtract {
206            high: *high,
207            low: *low,
208            operand: Box::new(replace_t_with_t1(operand)),
209        },
210        VerifyExpr::BitVecConcat(l, r) => VerifyExpr::BitVecConcat(
211            Box::new(replace_t_with_t1(l)),
212            Box::new(replace_t_with_t1(r)),
213        ),
214        VerifyExpr::Select { array, index } => VerifyExpr::Select {
215            array: Box::new(replace_t_with_t1(array)),
216            index: Box::new(replace_t_with_t1(index)),
217        },
218        VerifyExpr::Store { array, index, value } => VerifyExpr::Store {
219            array: Box::new(replace_t_with_t1(array)),
220            index: Box::new(replace_t_with_t1(index)),
221            value: Box::new(replace_t_with_t1(value)),
222        },
223        VerifyExpr::AtState { state, expr } => VerifyExpr::AtState {
224            state: Box::new(replace_t_with_t1(state)),
225            expr: Box::new(replace_t_with_t1(expr)),
226        },
227        VerifyExpr::Transition { from, to } => VerifyExpr::Transition {
228            from: Box::new(replace_t_with_t1(from)),
229            to: Box::new(replace_t_with_t1(to)),
230        },
231    }
232}
233
234/// CEGAR verification loop: abstract, check, refine.
235///
236/// 1. Build abstract model from current predicates
237/// 2. Check abstract model with k-induction
238/// 3. If abstract model is safe, return Safe
239/// 4. If abstract counterexample found, check concreteness via BMC
240/// 5. If concrete, return Unsafe
241/// 6. If spurious, extract new predicates from the counterexample and refine
242pub fn cegar_verify(
243    init: &VerifyExpr,
244    transition: &VerifyExpr,
245    property: &VerifyExpr,
246    initial_predicates: &[VerifyExpr],
247    max_refinements: u32,
248) -> AbstractionResult {
249    let mut predicates = initial_predicates.to_vec();
250
251    for _iteration in 0..max_refinements {
252        // Build the abstract model
253        let abs_model = abstract_model_full(init, transition, &predicates);
254
255        // Verify the abstract model using k-induction.
256        let abs_result = kinduction::k_induction(
257            &abs_model.abstract_init,
258            &abs_model.abstract_transition,
259            property,
260            &[],
261            10,
262        );
263
264        match abs_result {
265            kinduction::KInductionResult::Proven { .. } => {
266                // Abstract model proved property safe. Since predicate abstraction
267                // is an overapproximation (when inductive predicates are used as
268                // strengthening), safety of the abstract model implies safety of
269                // the concrete model.
270                return AbstractionResult::Safe;
271            }
272            kinduction::KInductionResult::Counterexample { trace: _, k } => {
273                // Abstract counterexample found. Check if it is concrete
274                // by running k-induction on the concrete model.
275                let concrete_result = kinduction::k_induction(
276                    init, transition, property, &[], k.max(10),
277                );
278
279                match concrete_result {
280                    kinduction::KInductionResult::Counterexample { trace: ctrace, .. } => {
281                        return AbstractionResult::Unsafe { concrete_trace: ctrace };
282                    }
283                    kinduction::KInductionResult::Proven { .. } => {
284                        // Spurious. Refine.
285                        let new_preds = extract_refinement_predicates(
286                            init, transition, property, &predicates, k,
287                        );
288                        if new_preds.is_empty() {
289                            return AbstractionResult::SpuriousRefined {
290                                new_predicates: predicates,
291                            };
292                        }
293                        predicates.extend(new_preds);
294                    }
295                    _ => {
296                        // Inconclusive — try refining.
297                        let new_preds = extract_refinement_predicates(
298                            init, transition, property, &predicates, k,
299                        );
300                        if new_preds.is_empty() {
301                            return AbstractionResult::SpuriousRefined {
302                                new_predicates: predicates,
303                            };
304                        }
305                        predicates.extend(new_preds);
306                    }
307                }
308            }
309            kinduction::KInductionResult::InductionFailed { k, .. } => {
310                // k-induction couldn't prove or disprove on abstract model.
311                // Fall back to concrete check.
312                let concrete_result = kinduction::k_induction(
313                    init, transition, property, &[], k.max(10),
314                );
315                match concrete_result {
316                    kinduction::KInductionResult::Proven { .. } => {
317                        return AbstractionResult::Safe;
318                    }
319                    kinduction::KInductionResult::Counterexample { trace, .. } => {
320                        return AbstractionResult::Unsafe { concrete_trace: trace };
321                    }
322                    _ => {
323                        let new_preds = extract_refinement_predicates(
324                            init, transition, property, &predicates, k,
325                        );
326                        if new_preds.is_empty() {
327                            return AbstractionResult::Unknown;
328                        }
329                        predicates.extend(new_preds);
330                    }
331                }
332            }
333            kinduction::KInductionResult::Unknown => {
334                return AbstractionResult::Unknown;
335            }
336        }
337    }
338
339    // Exhausted refinement budget — try one final concrete check
340    let final_result = kinduction::k_induction(init, transition, property, &[], 10);
341    match final_result {
342        kinduction::KInductionResult::Proven { .. } => AbstractionResult::Safe,
343        kinduction::KInductionResult::Counterexample { trace, .. } => {
344            AbstractionResult::Unsafe { concrete_trace: trace }
345        }
346        _ => AbstractionResult::Unknown,
347    }
348}
349
350/// Extract refinement predicates from a spurious counterexample.
351///
352/// When an abstract counterexample is spurious, we need new predicates to
353/// eliminate it. The strategy:
354///
355/// 1. Collect all subexpressions of the transition and property that are
356///    comparison predicates (>=, <=, >, <, ==, !=).
357/// 2. Filter out predicates already in the set.
358/// 3. Return the new ones as refinement candidates.
359///
360/// This is a simplified form of Craig interpolation — we extract predicates
361/// from the structure of the transition relation and property.
362fn extract_refinement_predicates(
363    init: &VerifyExpr,
364    transition: &VerifyExpr,
365    property: &VerifyExpr,
366    existing_predicates: &[VerifyExpr],
367    _depth: u32,
368) -> Vec<VerifyExpr> {
369    let mut candidates = Vec::new();
370
371    // Collect comparison subexpressions from transition, init, property
372    collect_comparison_predicates(transition, &mut candidates);
373    collect_comparison_predicates(init, &mut candidates);
374    collect_comparison_predicates(property, &mut candidates);
375
376    // Also generate weakened/strengthened versions of existing predicates
377    for pred in existing_predicates {
378        if let VerifyExpr::Binary { op, left, right } = pred {
379            match op {
380                VerifyOp::Gte | VerifyOp::Gt | VerifyOp::Lte | VerifyOp::Lt => {
381                    if let VerifyExpr::Int(n) = right.as_ref() {
382                        candidates.push(VerifyExpr::binary(*op, *left.clone(), VerifyExpr::int(n + 1)));
383                        candidates.push(VerifyExpr::binary(*op, *left.clone(), VerifyExpr::int(n - 1)));
384                    }
385                }
386                _ => {}
387            }
388        }
389    }
390
391    // Deduplicate and remove existing predicates (and remove the property itself)
392    let existing_dbg: HashSet<String> = existing_predicates.iter()
393        .map(|p| format!("{:?}", p))
394        .collect();
395
396    let mut seen = HashSet::new();
397    let mut result = Vec::new();
398    for candidate in candidates {
399        let dbg = format!("{:?}", candidate);
400        if !existing_dbg.contains(&dbg) && seen.insert(dbg) {
401            result.push(candidate);
402        }
403    }
404
405    result
406}
407
408/// Collect all comparison subexpressions from an expression.
409fn collect_comparison_predicates(expr: &VerifyExpr, out: &mut Vec<VerifyExpr>) {
410    match expr {
411        VerifyExpr::Binary { op, left, right } => {
412            match op {
413                VerifyOp::Gte | VerifyOp::Gt | VerifyOp::Lte | VerifyOp::Lt
414                | VerifyOp::Eq | VerifyOp::Neq => {
415                    out.push(expr.clone());
416                }
417                _ => {}
418            }
419            collect_comparison_predicates(left, out);
420            collect_comparison_predicates(right, out);
421        }
422        VerifyExpr::Not(inner) => collect_comparison_predicates(inner, out),
423        VerifyExpr::Iff(l, r) => {
424            collect_comparison_predicates(l, out);
425            collect_comparison_predicates(r, out);
426        }
427        VerifyExpr::ForAll { body, .. } | VerifyExpr::Exists { body, .. } => {
428            collect_comparison_predicates(body, out);
429        }
430        _ => {}
431    }
432}
433
434/// Encode a VerifyExpr to Z3 Bool (local helper, similar to ic3's encode_bool).
435fn encode_bool(expr: &VerifyExpr) -> Bool {
436    let mut bool_vars = HashMap::new();
437    let mut int_vars = HashMap::new();
438    let mut all_vars = HashSet::new();
439    crate::equivalence::collect_vars_pub(expr, &mut all_vars);
440    for name in &all_vars {
441        bool_vars.insert(name.clone(), Bool::new_const(name.as_str()));
442    }
443    crate::equivalence::collect_int_vars_pub(expr, &mut int_vars);
444    kinduction::encode_expr_bool(expr, &bool_vars, &int_vars)
445}