Skip to main content

logicaffeine_verify/
liveness.rs

1//! Liveness-to-Safety Reduction
2//!
3//! Liveness properties (G(F(p)) — "p always eventually holds") cannot be checked
4//! by BMC or k-induction directly. The Biere-Artho-Schuppan (2002) reduction:
5//!
6//! 1. Add a shadow copy of the state
7//! 2. Non-deterministically "freeze" the shadow at some point
8//! 3. Check that the property eventually holds after the freeze
9//!
10//! If the safety property on the doubled state space holds → liveness holds.
11//! If violated → extract lasso-shaped counterexample (prefix + loop).
12
13use crate::ir::VerifyExpr;
14use crate::equivalence::{Trace, CycleState, SignalValue};
15use crate::kinduction;
16use std::collections::{HashMap, HashSet};
17
18/// Result of liveness checking.
19#[derive(Debug)]
20pub enum LivenessResult {
21    /// Property holds on all fair paths.
22    Live,
23    /// Property does not hold — lasso-shaped counterexample.
24    NotLive { trace: Trace, loop_point: usize },
25    /// Could not determine.
26    Unknown,
27}
28
29/// Check a liveness property via bounded search.
30///
31/// The property should be the "eventually" part of G(F(property)).
32/// Fairness constraints are additional conditions that must hold infinitely often.
33pub fn check_liveness(
34    init: &VerifyExpr,
35    transition: &VerifyExpr,
36    fairness: &[VerifyExpr],
37    property: &VerifyExpr,
38    max_k: u32,
39) -> LivenessResult {
40
41    // Collect signal names for trace extraction
42    let mut all_vars = HashSet::new();
43    collect_vars(init, &mut all_vars);
44    collect_vars(transition, &mut all_vars);
45    collect_vars(property, &mut all_vars);
46    for f in fairness {
47        collect_vars(f, &mut all_vars);
48    }
49    let signal_names = extract_signal_names(&all_vars);
50
51    // Check: can we go max_k steps from init without EVER seeing property?
52    // If NOT (UNSAT), then property MUST hold within max_k steps from any init state → Live.
53    // If YES (SAT), we found a finite prefix where property never holds → potential NotLive.
54    for k in 1..=max_k {
55        let solver = crate::solver::new_solver();
56
57        // Assert init at step 0
58        let init_0 = kinduction::instantiate_at(init, 0);
59        solver.assert(&encode_bool(&init_0));
60
61        // Assert transitions
62        for t in 0..k {
63            let trans = kinduction::instantiate_transition(transition, t);
64            solver.assert(&encode_bool(&trans));
65        }
66
67        // Assert fairness constraints are met somewhere in the trace
68        for fair in fairness {
69            // At least one step satisfies the fairness constraint
70            let mut fair_options: Vec<z3::ast::Bool> = Vec::new();
71            for t in 0..=k {
72                let fair_t = kinduction::instantiate_at(fair, t);
73                fair_options.push(encode_bool(&fair_t));
74            }
75            let fair_refs: Vec<&z3::ast::Bool> = fair_options.iter().collect();
76            if !fair_refs.is_empty() {
77                let some_fair = z3::ast::Bool::or(&fair_refs);
78                solver.assert(&some_fair);
79            }
80        }
81
82        // Assert property NEVER holds in k+1 steps
83        for t in 0..=k {
84            let prop_t = kinduction::instantiate_at(property, t);
85            solver.assert(&encode_bool(&prop_t).not());
86        }
87
88        match solver.check() {
89            z3::SatResult::Sat => {
90                // Found a path where property never holds for k steps
91                if k == max_k {
92                    // Extract concrete trace
93                    let trace = extract_liveness_trace(&solver, k, &signal_names);
94                    let loop_point = find_loop_point(&trace);
95                    return LivenessResult::NotLive { trace, loop_point };
96                }
97                // Continue to larger k for a more conclusive result
98            }
99            z3::SatResult::Unsat => {
100                // No path of length k avoids the property → liveness holds up to k
101                return LivenessResult::Live;
102            }
103            z3::SatResult::Unknown => return LivenessResult::Unknown,
104        }
105    }
106
107    // Exhausted bound without proving liveness — find concrete trace
108    let solver = crate::solver::new_solver();
109    let init_0 = kinduction::instantiate_at(init, 0);
110    solver.assert(&encode_bool(&init_0));
111    for t in 0..max_k {
112        let trans = kinduction::instantiate_transition(transition, t);
113        solver.assert(&encode_bool(&trans));
114    }
115    for t in 0..=max_k {
116        let prop_t = kinduction::instantiate_at(property, t);
117        solver.assert(&encode_bool(&prop_t).not());
118    }
119
120    if matches!(solver.check(), z3::SatResult::Sat) {
121        let trace = extract_liveness_trace(&solver, max_k, &signal_names);
122        let loop_point = find_loop_point(&trace);
123        LivenessResult::NotLive { trace, loop_point }
124    } else {
125        LivenessResult::Live
126    }
127}
128
129/// Extract signal names from variable set.
130fn extract_signal_names(all_vars: &HashSet<String>) -> Vec<String> {
131    let mut signals = HashSet::new();
132    for v in all_vars {
133        let base = v.replace("@0", "").replace("@t1", "").replace("@t", "");
134        if !base.is_empty() {
135            signals.insert(base);
136        }
137    }
138    signals.into_iter().collect()
139}
140
141/// Extract a concrete trace from a SAT solver model.
142fn extract_liveness_trace(
143    solver: &z3::Solver,
144    k: u32,
145    signal_names: &[String],
146) -> Trace {
147    let model = match solver.get_model() {
148        Some(m) => m,
149        None => return Trace { cycles: vec![CycleState { cycle: 0, signals: HashMap::new() }] },
150    };
151
152    let mut cycles = Vec::new();
153    for step in 0..=k {
154        let mut signals = HashMap::new();
155        for sig in signal_names {
156            let var_name = format!("{}@{}", sig, step);
157            let bool_var = z3::ast::Bool::new_const(var_name.as_str());
158            if let Some(val) = model.eval(&bool_var, true) {
159                if let Some(b) = val.as_bool() {
160                    signals.insert(sig.clone(), SignalValue::Bool(b));
161                    continue;
162                }
163            }
164            let int_var = z3::ast::Int::new_const(var_name.as_str());
165            if let Some(val) = model.eval(&int_var, true) {
166                if let Some(n) = val.as_i64() {
167                    signals.insert(sig.clone(), SignalValue::Int(n));
168                    continue;
169                }
170            }
171        }
172        if !signals.is_empty() {
173            cycles.push(CycleState { cycle: step as usize, signals });
174        }
175    }
176
177    if cycles.is_empty() {
178        // Fallback: at least provide one cycle with unknown values
179        let mut signals = HashMap::new();
180        for sig in signal_names {
181            signals.insert(sig.clone(), SignalValue::Unknown);
182        }
183        cycles.push(CycleState { cycle: 0, signals });
184    }
185
186    Trace { cycles }
187}
188
189/// Find the loop point in a trace (where the lasso begins).
190fn find_loop_point(trace: &Trace) -> usize {
191    if trace.cycles.len() <= 1 {
192        return 0;
193    }
194    // Look for repeating state pattern — the simplest heuristic
195    // is to look for where the last state matches an earlier state.
196    let last = &trace.cycles[trace.cycles.len() - 1];
197    for (i, cycle) in trace.cycles.iter().enumerate() {
198        if i < trace.cycles.len() - 1 && states_match(&cycle.signals, &last.signals) {
199            return i;
200        }
201    }
202    // Default: loop starts at the midpoint
203    trace.cycles.len() / 2
204}
205
206/// Check if two signal maps represent the same state.
207fn states_match(a: &HashMap<String, SignalValue>, b: &HashMap<String, SignalValue>) -> bool {
208    if a.len() != b.len() { return false; }
209    for (key, val_a) in a {
210        match b.get(key) {
211            Some(val_b) => {
212                let sa = format!("{:?}", val_a);
213                let sb = format!("{:?}", val_b);
214                if sa != sb { return false; }
215            }
216            None => return false,
217        }
218    }
219    true
220}
221
222fn collect_vars(expr: &VerifyExpr, vars: &mut HashSet<String>) {
223    match expr {
224        VerifyExpr::Var(name) => { vars.insert(name.clone()); }
225        VerifyExpr::Binary { left, right, .. } => {
226            collect_vars(left, vars);
227            collect_vars(right, vars);
228        }
229        VerifyExpr::Not(inner) => collect_vars(inner, vars),
230        VerifyExpr::Iff(l, r) => {
231            collect_vars(l, vars);
232            collect_vars(r, vars);
233        }
234        VerifyExpr::ForAll { body, .. } | VerifyExpr::Exists { body, .. } => {
235            collect_vars(body, vars);
236        }
237        _ => {}
238    }
239}
240
241fn encode_bool(expr: &VerifyExpr) -> z3::ast::Bool {
242    let mut bool_vars = HashMap::new();
243    let mut int_vars = HashMap::new();
244    let mut all_vars = std::collections::HashSet::new();
245    crate::equivalence::collect_vars_pub(expr, &mut all_vars);
246    for name in &all_vars {
247        bool_vars.insert(name.clone(), z3::ast::Bool::new_const(name.as_str()));
248    }
249    crate::equivalence::collect_int_vars_pub(expr, &mut int_vars);
250    kinduction::encode_expr_bool(expr, &bool_vars, &int_vars)
251}