Skip to main content

logicaffeine_verify/
consistency.rs

1//! Multi-Property Consistency Checking
2//!
3//! Check if a set of properties can all hold simultaneously.
4//! Conjoin all properties, check Z3 satisfiability.
5//! If UNSAT → extract minimal unsatisfiable subset (MUS).
6//! If SAT → check for vacuity and redundancy.
7
8use crate::ir::{VerifyExpr, VerifyOp};
9use crate::equivalence::Trace;
10use std::collections::{HashMap, HashSet};
11use z3::{ast::Bool, ast::Int, SatResult, Solver};
12
13// ═══════════════════════════════════════════════════════════════════════════
14// LEGACY API (backward compatibility)
15// ═══════════════════════════════════════════════════════════════════════════
16
17/// Result of consistency checking (legacy API).
18#[derive(Debug)]
19pub enum ConsistencyResult {
20    /// All properties can hold simultaneously.
21    Consistent,
22    /// At least two properties conflict. Returns the conflicting pair indices
23    /// and a witness trace showing the contradiction.
24    Inconsistent {
25        conflicting: Vec<(usize, usize)>,
26        witness: Trace,
27    },
28    /// Z3 returned unknown.
29    Unknown,
30}
31
32/// Check if a set of properties can all hold simultaneously (legacy API).
33///
34/// Conjoins all properties and checks Z3 satisfiability.
35/// If UNSAT, identifies the conflicting pair(s).
36pub fn check_consistency(
37    props: &[VerifyExpr],
38    signals: &[String],
39    bound: usize,
40) -> ConsistencyResult {
41    if props.is_empty() {
42        return ConsistencyResult::Consistent;
43    }
44
45    let solver = crate::solver::new_solver();
46
47    // Declare all signal@timestep variables
48    let mut var_map: HashMap<String, Bool> = HashMap::new();
49    for sig in signals {
50        for t in 0..=bound {
51            let var_name = format!("{}@{}", sig, t);
52            var_map.insert(var_name.clone(), Bool::new_const(var_name.as_str()));
53        }
54    }
55
56    // Collect all variables from all properties
57    let mut all_vars: HashSet<String> = HashSet::new();
58    for prop in props {
59        crate::equivalence::collect_vars_pub(prop, &mut all_vars);
60    }
61    for var_name in &all_vars {
62        if !var_map.contains_key(var_name) {
63            var_map.insert(var_name.clone(), Bool::new_const(var_name.as_str()));
64        }
65    }
66
67    // Encode and conjoin all properties
68    let empty_int_vars = HashMap::new();
69    let empty_bv_vars = HashMap::new();
70    let empty_array_vars = HashMap::new();
71    let encoder = crate::equivalence::EquivEncoder::new(&var_map, &empty_int_vars, &empty_bv_vars, &empty_array_vars);
72    for prop in props {
73        let encoded = encoder.encode_as_bool(prop);
74        solver.assert(&encoded);
75    }
76
77    match solver.check() {
78        SatResult::Sat => ConsistencyResult::Consistent,
79        SatResult::Unsat => {
80            // Find conflicting pairs by checking each pair
81            let mut conflicting = Vec::new();
82            for i in 0..props.len() {
83                for j in (i + 1)..props.len() {
84                    let pair_solver = crate::solver::new_solver();
85                    let ei = encoder.encode_as_bool(&props[i]);
86                    let ej = encoder.encode_as_bool(&props[j]);
87                    pair_solver.assert(&ei);
88                    pair_solver.assert(&ej);
89                    if pair_solver.check() == SatResult::Unsat {
90                        conflicting.push((i, j));
91                    }
92                }
93            }
94            ConsistencyResult::Inconsistent {
95                conflicting,
96                witness: Trace { cycles: vec![] },
97            }
98        }
99        SatResult::Unknown => ConsistencyResult::Unknown,
100    }
101}
102
103// ═══════════════════════════════════════════════════════════════════════════
104// NEW API: Spec Consistency Analysis
105// ═══════════════════════════════════════════════════════════════════════════
106
107/// A formula labeled with its source text for error reporting.
108#[derive(Debug, Clone)]
109pub struct LabeledFormula {
110    pub index: usize,
111    pub label: String,
112    pub expr: VerifyExpr,
113}
114
115/// Configuration for spec consistency analysis.
116#[derive(Debug, Clone)]
117pub struct ConsistencyConfig {
118    /// Z3 timeout per query in milliseconds.
119    pub timeout_ms: u64,
120    /// BMC bound for temporal unrolling (used by orchestrator, not by Z3 checks).
121    pub temporal_bound: u32,
122    /// Whether to check for vacuous implications.
123    pub check_vacuity: bool,
124    /// Whether to check for redundant formulas.
125    pub check_redundancy: bool,
126    /// Whether to identify pairwise conflicts (only runs when UNSAT).
127    pub check_pairwise: bool,
128}
129
130impl Default for ConsistencyConfig {
131    fn default() -> Self {
132        Self {
133            timeout_ms: 5000,
134            temporal_bound: 8,
135            check_vacuity: true,
136            check_redundancy: true,
137            check_pairwise: true,
138        }
139    }
140}
141
142/// Full consistency analysis report.
143#[derive(Debug)]
144pub struct ConsistencyReport {
145    pub satisfiability: SatisfiabilityResult,
146    pub vacuity: Vec<VacuityFinding>,
147    pub redundancies: Vec<RedundancyFinding>,
148    pub pairwise_conflicts: Vec<PairwiseConflict>,
149}
150
151/// Whether the conjunction of all formulas is satisfiable.
152#[derive(Debug, PartialEq)]
153pub enum SatisfiabilityResult {
154    /// All formulas can hold simultaneously.
155    Satisfiable,
156    /// The formulas are contradictory. `mus` contains the indices of
157    /// the Minimal Unsatisfiable Subset.
158    Unsatisfiable { mus: Vec<usize> },
159    /// Z3 returned unknown (timeout or undecidable).
160    Unknown,
161}
162
163/// A formula whose implication antecedent can never be satisfied.
164#[derive(Debug)]
165pub struct VacuityFinding {
166    pub formula_index: usize,
167    pub label: String,
168}
169
170/// A formula that is logically entailed by the remaining formulas.
171#[derive(Debug)]
172pub struct RedundancyFinding {
173    pub redundant_index: usize,
174    pub label: String,
175    pub entailed_by: Vec<usize>,
176}
177
178/// A pair of formulas that cannot hold simultaneously.
179#[derive(Debug)]
180pub struct PairwiseConflict {
181    pub i: usize,
182    pub j: usize,
183}
184
185// ═══════════════════════════════════════════════════════════════════════════
186// CORE ANALYSIS ENGINE
187// ═══════════════════════════════════════════════════════════════════════════
188
189/// Run full consistency analysis on a set of labeled formulas.
190///
191/// Orchestration order:
192/// 1. Check full conjunction satisfiability
193/// 2. If UNSAT → extract MUS, optionally run pairwise
194/// 3. If SAT → optionally run vacuity and redundancy checks
195pub fn check_spec_consistency(
196    formulas: &[LabeledFormula],
197    config: &ConsistencyConfig,
198) -> ConsistencyReport {
199    if formulas.is_empty() {
200        return ConsistencyReport {
201            satisfiability: SatisfiabilityResult::Satisfiable,
202            vacuity: vec![],
203            redundancies: vec![],
204            pairwise_conflicts: vec![],
205        };
206    }
207
208
209    // Collect all variable names from all formulas
210    let mut all_vars: HashSet<String> = HashSet::new();
211    for lf in formulas {
212        crate::equivalence::collect_vars_pub(&lf.expr, &mut all_vars);
213    }
214
215    // Declare all variables as Z3 Bools, and collect integer variables
216    let mut bool_vars: HashMap<String, Bool> = HashMap::new();
217    let mut int_vars: HashMap<String, Int> = HashMap::new();
218    for var_name in &all_vars {
219        bool_vars.insert(
220            var_name.clone(),
221            Bool::new_const(var_name.as_str()),
222        );
223    }
224    // Also detect integer variables from arithmetic contexts
225    let exprs: Vec<&VerifyExpr> = formulas.iter().map(|lf| &lf.expr).collect();
226    for expr in &exprs {
227        crate::equivalence::collect_int_vars_pub(expr, &mut int_vars);
228    }
229
230    let empty_bv_vars = HashMap::new();
231    let empty_array_vars = HashMap::new();
232    let encoder = crate::equivalence::EquivEncoder::new(&bool_vars, &int_vars, &empty_bv_vars, &empty_array_vars);
233
234    // Step 1: Check full conjunction satisfiability
235    let solver = crate::solver::new_solver();
236    for lf in formulas {
237        let encoded = encoder.encode_as_bool(&lf.expr);
238        solver.assert(&encoded);
239    }
240
241    match solver.check() {
242        SatResult::Sat => {
243            // Satisfiable — run vacuity and redundancy checks
244            let vacuity = if config.check_vacuity {
245                detect_vacuity(&encoder, formulas, config.timeout_ms)
246            } else {
247                vec![]
248            };
249
250            let redundancies = if config.check_redundancy {
251                detect_redundancy(&encoder, formulas, config.timeout_ms)
252            } else {
253                vec![]
254            };
255
256            // Skip pairwise when SAT — no pair can conflict if the full conjunction is SAT
257            ConsistencyReport {
258                satisfiability: SatisfiabilityResult::Satisfiable,
259                vacuity,
260                redundancies,
261                pairwise_conflicts: vec![],
262            }
263        }
264        SatResult::Unsat => {
265            // UNSAT — extract MUS
266            let mus = extract_mus(&encoder, formulas, config.timeout_ms);
267
268            // Optionally find all pairwise conflicts
269            let pairwise_conflicts = if config.check_pairwise {
270                detect_pairwise_conflicts(&encoder, formulas, config.timeout_ms)
271            } else {
272                vec![]
273            };
274
275            ConsistencyReport {
276                satisfiability: SatisfiabilityResult::Unsatisfiable { mus },
277                vacuity: vec![],
278                redundancies: vec![],
279                pairwise_conflicts,
280            }
281        }
282        SatResult::Unknown => {
283            ConsistencyReport {
284                satisfiability: SatisfiabilityResult::Unknown,
285                vacuity: vec![],
286                redundancies: vec![],
287                pairwise_conflicts: vec![],
288            }
289        }
290    }
291}
292
293// ═══════════════════════════════════════════════════════════════════════════
294// MUS EXTRACTION
295// ═══════════════════════════════════════════════════════════════════════════
296
297/// Deletion-based Minimal Unsatisfiable Subset extraction.
298///
299/// Precondition: the conjunction of all formulas is UNSAT.
300/// For each formula, checks whether removing it makes the remaining set SAT.
301/// If so, the formula is necessary for the contradiction (kept in MUS).
302/// If not, the formula is not necessary (removed from MUS).
303///
304/// O(n) Z3 calls.
305fn extract_mus(
306    encoder: &crate::equivalence::EquivEncoder,
307    formulas: &[LabeledFormula],
308    timeout_ms: u64,
309) -> Vec<usize> {
310    let n = formulas.len();
311    let mut in_mus: Vec<bool> = vec![true; n];
312
313    for i in 0..n {
314        // Try removing formula i — check if the rest is still UNSAT
315        let solver = Solver::new();
316        solver.set_params(&{
317            let mut params = z3::Params::new();
318            params.set_u32("timeout", timeout_ms as u32);
319            params
320        });
321
322        for j in 0..n {
323            if j == i || !in_mus[j] {
324                continue;
325            }
326            let encoded = encoder.encode_as_bool(&formulas[j].expr);
327            solver.assert(&encoded);
328        }
329
330        match solver.check() {
331            SatResult::Sat => {
332                // Removing i made it SAT → i is necessary for UNSAT → keep in MUS
333            }
334            SatResult::Unsat => {
335                // Still UNSAT without i → i is not necessary → remove from MUS
336                in_mus[i] = false;
337            }
338            SatResult::Unknown => {
339                // Conservative: keep it in MUS
340            }
341        }
342    }
343
344    in_mus.iter().enumerate()
345        .filter(|(_, &b)| b)
346        .map(|(i, _)| i)
347        .collect()
348}
349
350// ═══════════════════════════════════════════════════════════════════════════
351// VACUITY DETECTION
352// ═══════════════════════════════════════════════════════════════════════════
353
354/// Extract the antecedent from an implication-shaped formula.
355///
356/// Looks through ForAll wrappers to find the implication core.
357fn extract_antecedent(expr: &VerifyExpr) -> Option<&VerifyExpr> {
358    match expr {
359        VerifyExpr::Binary { op: VerifyOp::Implies, left, .. } => Some(left),
360        VerifyExpr::ForAll { body, .. } => extract_antecedent(body),
361        _ => None,
362    }
363}
364
365/// Detect vacuously true implications.
366///
367/// For each formula with implication shape (P → Q), checks whether P
368/// can be satisfied under the context of all other formulas.
369/// If not, the implication is vacuously true.
370fn detect_vacuity(
371    encoder: &crate::equivalence::EquivEncoder,
372    formulas: &[LabeledFormula],
373    timeout_ms: u64,
374) -> Vec<VacuityFinding> {
375    let mut findings = Vec::new();
376
377    for i in 0..formulas.len() {
378        let antecedent = match extract_antecedent(&formulas[i].expr) {
379            Some(a) => a,
380            None => continue,
381        };
382
383        // Build context: conjunction of all other formulas
384        let solver = Solver::new();
385        solver.set_params(&{
386            let mut params = z3::Params::new();
387            params.set_u32("timeout", timeout_ms as u32);
388            params
389        });
390
391        for j in 0..formulas.len() {
392            if j == i {
393                continue;
394            }
395            let encoded = encoder.encode_as_bool(&formulas[j].expr);
396            solver.assert(&encoded);
397        }
398
399        // Assert the antecedent — can it be true?
400        let ante_encoded = encoder.encode_as_bool(antecedent);
401        solver.assert(&ante_encoded);
402
403        if solver.check() == SatResult::Unsat {
404            findings.push(VacuityFinding {
405                formula_index: i,
406                label: formulas[i].label.clone(),
407            });
408        }
409    }
410
411    findings
412}
413
414// ═══════════════════════════════════════════════════════════════════════════
415// REDUNDANCY DETECTION
416// ═══════════════════════════════════════════════════════════════════════════
417
418/// Detect formulas that are logically entailed by the remaining formulas.
419///
420/// For each formula F_i, checks whether context ∧ ¬F_i is satisfiable.
421/// If UNSAT, F_i is entailed by the others (redundant).
422fn detect_redundancy(
423    encoder: &crate::equivalence::EquivEncoder,
424    formulas: &[LabeledFormula],
425    timeout_ms: u64,
426) -> Vec<RedundancyFinding> {
427    let mut findings = Vec::new();
428
429    for i in 0..formulas.len() {
430        let solver = Solver::new();
431        solver.set_params(&{
432            let mut params = z3::Params::new();
433            params.set_u32("timeout", timeout_ms as u32);
434            params
435        });
436
437        // Assert all other formulas as context
438        for j in 0..formulas.len() {
439            if j == i {
440                continue;
441            }
442            let encoded = encoder.encode_as_bool(&formulas[j].expr);
443            solver.assert(&encoded);
444        }
445
446        // Assert the negation of F_i
447        let fi_encoded = encoder.encode_as_bool(&formulas[i].expr);
448        solver.assert(&fi_encoded.not());
449
450        if solver.check() == SatResult::Unsat {
451            // context ∧ ¬F_i is UNSAT → F_i is entailed by others
452            let entailed_by: Vec<usize> = (0..formulas.len())
453                .filter(|&j| j != i)
454                .collect();
455            findings.push(RedundancyFinding {
456                redundant_index: i,
457                label: formulas[i].label.clone(),
458                entailed_by,
459            });
460        }
461    }
462
463    findings
464}
465
466// ═══════════════════════════════════════════════════════════════════════════
467// PAIRWISE CONFLICT DETECTION
468// ═══════════════════════════════════════════════════════════════════════════
469
470/// Identify all pairs of formulas that cannot hold simultaneously.
471///
472/// Only meaningful when the full conjunction is UNSAT.
473fn detect_pairwise_conflicts(
474    encoder: &crate::equivalence::EquivEncoder,
475    formulas: &[LabeledFormula],
476    timeout_ms: u64,
477) -> Vec<PairwiseConflict> {
478    let mut conflicts = Vec::new();
479
480    for i in 0..formulas.len() {
481        for j in (i + 1)..formulas.len() {
482            let solver = Solver::new();
483            solver.set_params(&{
484                let mut params = z3::Params::new();
485                params.set_u32("timeout", timeout_ms as u32);
486                params
487            });
488
489            let ei = encoder.encode_as_bool(&formulas[i].expr);
490            let ej = encoder.encode_as_bool(&formulas[j].expr);
491            solver.assert(&ei);
492            solver.assert(&ej);
493
494            if solver.check() == SatResult::Unsat {
495                conflicts.push(PairwiseConflict { i, j });
496            }
497        }
498    }
499
500    conflicts
501}