Skip to main content

logicaffeine_compile/codegen_sva/
hw_pipeline.rs

1//! Hardware Verification Pipeline
2//!
3//! Public API for the LOGOS hardware verification pipeline:
4//! English spec → Kripke FOL → Knowledge Graph → SVA → Z3 Equivalence.
5
6use super::sva_model::{SvaExpr, parse_sva, sva_expr_to_string, sva_exprs_structurally_equivalent};
7use super::sva_to_verify::{SvaTranslator, BoundedExpr, TranslateResult};
8use super::fol_to_verify::FolTranslator;
9use super::{SvaProperty, SvaAssertionKind, emit_sva_property, sanitize_property_name};
10use logicaffeine_language::semantics::knowledge_graph::{HwKnowledgeGraph, SignalRole};
11use std::collections::HashMap;
12
13// ═══════════════════════════════════════════════════════════════════════════
14// SIGNAL MAP
15// ═══════════════════════════════════════════════════════════════════════════
16
17/// Maps FOL argument names (from English proper nouns) to SVA signal names.
18///
19/// When the English spec says "Req is valid", LOGOS produces `Valid(Req, w)`.
20/// The signal map translates `Req` → `req` so the bounded variable becomes
21/// `req@t` instead of `Valid_Req_@t`, matching the SVA side.
22#[derive(Debug, Clone)]
23pub struct SignalMap {
24    map: HashMap<String, String>,
25}
26
27impl SignalMap {
28    pub fn new() -> Self {
29        Self { map: HashMap::new() }
30    }
31
32    /// Add a mapping from FOL argument name to SVA signal name.
33    pub fn add(&mut self, fol_arg: &str, sva_signal: &str) {
34        self.map.insert(fol_arg.to_string(), sva_signal.to_string());
35    }
36
37    /// Resolve a FOL argument name to its SVA signal name.
38    pub fn resolve(&self, fol_arg: &str) -> Option<&str> {
39        self.map.get(fol_arg).map(|s| s.as_str())
40    }
41
42    /// Build a signal map from hardware signal declarations.
43    pub fn from_decls(decls: &[HwSignalDecl]) -> Self {
44        let mut map = Self::new();
45        for decl in decls {
46            map.add(&decl.english_name, &decl.sva_name);
47        }
48        map
49    }
50}
51
52// ═══════════════════════════════════════════════════════════════════════════
53// HARDWARE SIGNAL DECLARATION
54// ═══════════════════════════════════════════════════════════════════════════
55
56/// Declares a hardware signal with its English name and SVA name.
57///
58/// The `english_name` should be a capitalized proper noun used in the English spec
59/// (e.g., "Req", "Ack", "Awvalid"). The `sva_name` is the corresponding SVA signal
60/// (e.g., "req", "ack", "AWVALID").
61#[derive(Debug, Clone)]
62pub struct HwSignalDecl {
63    pub english_name: String,
64    pub sva_name: String,
65    pub width: u32,
66    pub role: SignalRole,
67}
68
69impl HwSignalDecl {
70    pub fn new(english_name: &str, sva_name: &str, width: u32, role: SignalRole) -> Self {
71        Self {
72            english_name: english_name.to_string(),
73            sva_name: sva_name.to_string(),
74            width,
75            role,
76        }
77    }
78}
79
80// ═══════════════════════════════════════════════════════════════════════════
81// PIPELINE TYPES
82// ═══════════════════════════════════════════════════════════════════════════
83
84/// Result of checking semantic equivalence between FOL and SVA.
85#[derive(Debug)]
86pub struct EquivalenceResult {
87    /// Whether the properties are equivalent at the given bound.
88    pub equivalent: bool,
89    /// If not equivalent, counterexample signal assignments (name@timestep → value).
90    pub counterexample: Option<Vec<(String, String)>>,
91    /// The BMC bound used for checking.
92    pub bound: u32,
93}
94
95/// A compiled hardware specification.
96#[derive(Debug)]
97pub struct HwSpec {
98    /// Kripke-lowered FOL as formatted text.
99    pub fol_text: String,
100    /// Knowledge graph extracted from the spec.
101    pub kg: HwKnowledgeGraph,
102}
103
104/// Full pipeline result.
105#[derive(Debug)]
106pub struct PipelineResult {
107    /// Property name.
108    pub property_name: String,
109    /// Equivalence result.
110    pub result: EquivalenceResult,
111    /// Generated SVA text.
112    pub sva_text: String,
113    /// FOL text from the spec.
114    pub fol_text: String,
115}
116
117/// Error type for hardware verification pipeline.
118#[derive(Debug)]
119pub enum HwError {
120    ParseError(String),
121    SvaParseError(String),
122    VerificationError(String),
123}
124
125impl std::fmt::Display for HwError {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        match self {
128            HwError::ParseError(msg) => write!(f, "Parse error: {}", msg),
129            HwError::SvaParseError(msg) => write!(f, "SVA parse error: {}", msg),
130            HwError::VerificationError(msg) => write!(f, "Verification error: {}", msg),
131        }
132    }
133}
134
135// ═══════════════════════════════════════════════════════════════════════════
136// PIPELINE FUNCTIONS
137// ═══════════════════════════════════════════════════════════════════════════
138
139/// Check structural equivalence between two SVA expressions.
140pub fn check_structural_equivalence(sva_a: &str, sva_b: &str) -> Result<bool, HwError> {
141    let expr_a = parse_sva(sva_a).map_err(|e| HwError::SvaParseError(e.message))?;
142    let expr_b = parse_sva(sva_b).map_err(|e| HwError::SvaParseError(e.message))?;
143    Ok(sva_exprs_structurally_equivalent(&expr_a, &expr_b))
144}
145
146/// Check bounded equivalence between two BoundedExpr trees.
147pub fn check_bounded_equivalence(
148    fol_bounded: &BoundedExpr,
149    sva_bounded: &BoundedExpr,
150    bound: u32,
151) -> EquivalenceResult {
152    let equivalent = bounded_exprs_equal(fol_bounded, sva_bounded);
153    EquivalenceResult {
154        equivalent,
155        counterexample: None,
156        bound,
157    }
158}
159
160/// Translate an SVA string to bounded verification IR.
161///
162/// Uses translate_property (G-wrapping) to model `assert property` semantics.
163pub fn translate_sva_to_bounded(sva_text: &str, bound: u32) -> Result<TranslateResult, HwError> {
164    let sva_expr = parse_sva(sva_text).map_err(|e| HwError::SvaParseError(e.message))?;
165    let mut translator = SvaTranslator::new(bound);
166    let result = translator.translate_property(&sva_expr);
167    Ok(result)
168}
169
170/// Translate SVA with smart G-wrapping for equivalence checking.
171/// If the outermost SVA node is already temporal (s_eventually, nexttime),
172/// translates at t=0 without G-wrapping. Otherwise uses translate_property.
173fn translate_sva_for_equiv(sva_text: &str, bound: u32) -> Result<TranslateResult, HwError> {
174    let sva_expr = parse_sva(sva_text).map_err(|e| HwError::SvaParseError(e.message))?;
175    let mut translator = SvaTranslator::new(bound);
176    if sva_has_outermost_temporal(&sva_expr) {
177        let expr = translator.translate(&sva_expr, 0);
178        let declarations: Vec<String> = translator.declarations.iter().cloned().collect();
179        Ok(TranslateResult { expr, declarations })
180    } else {
181        Ok(translator.translate_property(&sva_expr))
182    }
183}
184
185/// Check if the outermost SVA node is a temporal operator that already encodes
186/// the temporal unrolling (s_eventually, nexttime). These should NOT be wrapped
187/// in an additional G (conjunction over all timesteps).
188fn sva_has_outermost_temporal(expr: &SvaExpr) -> bool {
189    matches!(expr, SvaExpr::SEventually(_) | SvaExpr::Nexttime(_, _) | SvaExpr::SAlways(_))
190}
191
192/// Translate a LOGOS spec to bounded verification IR using compile_kripke_with.
193pub fn translate_spec_to_bounded(
194    spec: &str,
195    bound: u32,
196) -> Result<TranslateResult, HwError> {
197    logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
198        let mut translator = FolTranslator::new(interner, bound);
199        translator.translate_property(ast)
200    })
201    .map_err(|e| HwError::ParseError(format!("{:?}", e)))
202}
203
204/// Compile an English hardware property with signal declarations.
205///
206/// Takes an English specification and a list of signal declarations that map
207/// English proper nouns to SVA signal names. Produces a BoundedExpr with
208/// correctly-mapped signal variables.
209pub fn compile_hw_property(
210    spec: &str,
211    decls: &[HwSignalDecl],
212    bound: u32,
213) -> Result<TranslateResult, HwError> {
214    let signal_map = SignalMap::from_decls(decls);
215    logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
216        let mut translator = FolTranslator::new(interner, bound);
217        translator.set_signal_map(&signal_map);
218        translator.translate_property(ast)
219    })
220    .map_err(|e| HwError::ParseError(format!("{:?}", e)))
221}
222
223/// Compile an English hardware spec to FOL text.
224pub fn compile_hw_spec(source: &str) -> Result<String, HwError> {
225    logicaffeine_language::compile_kripke(source)
226        .map_err(|e| HwError::ParseError(format!("{:?}", e)))
227}
228
229/// Emit SVA from a property specification.
230pub fn emit_hw_sva(name: &str, clock: &str, body: &str, kind: SvaAssertionKind) -> String {
231    let prop = SvaProperty {
232        name: sanitize_property_name(name),
233        clock: clock.to_string(),
234        body: body.to_string(),
235        kind,
236    };
237    emit_sva_property(&prop)
238}
239
240/// Extract a Knowledge Graph from an English hardware spec (one call).
241pub fn extract_kg(spec: &str) -> Result<HwKnowledgeGraph, HwError> {
242    logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
243        logicaffeine_language::semantics::knowledge_graph::extract_from_kripke_ast(ast, interner)
244    })
245    .map_err(|e| HwError::ParseError(format!("{:?}", e)))
246}
247
248/// Check Z3 semantic equivalence between an English spec and an SVA string.
249///
250/// This is the core contribution — nobody else does this.
251#[cfg(feature = "verification")]
252pub fn check_z3_equivalence(
253    spec_source: &str,
254    sva_text: &str,
255    bound: u32,
256) -> Result<logicaffeine_verify::equivalence::EquivalenceResult, HwError> {
257    use super::sva_to_verify::{bounded_to_verify, extract_signal_names};
258
259    let spec_bounded = translate_spec_to_bounded(spec_source, bound)?;
260    let spec_verify = bounded_to_verify(&spec_bounded.expr);
261
262    let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
263    let sva_verify = bounded_to_verify(&sva_bounded.expr);
264
265    let mut all_signals = extract_signal_names(&spec_bounded);
266    let sva_signals = extract_signal_names(&sva_bounded);
267    for sig in sva_signals {
268        if !all_signals.contains(&sig) {
269            all_signals.push(sig);
270        }
271    }
272
273    Ok(logicaffeine_verify::equivalence::check_equivalence(
274        &spec_verify, &sva_verify, &all_signals, bound as usize,
275    ))
276}
277
278/// Check Z3 semantic equivalence with hardware signal declarations.
279///
280/// This is the signal-bridge version that maps English proper nouns to SVA signal
281/// names via HwSignalDecl. Both sides translate to the same variable namespace.
282#[cfg(feature = "verification")]
283pub fn check_z3_hw_equivalence(
284    spec: &str,
285    sva_text: &str,
286    decls: &[HwSignalDecl],
287    bound: u32,
288) -> Result<logicaffeine_verify::equivalence::EquivalenceResult, HwError> {
289    use super::sva_to_verify::{bounded_to_verify, extract_signal_names};
290
291    // 1. Compile English with signal map
292    let spec_bounded = compile_hw_property(spec, decls, bound)?;
293    let spec_verify = bounded_to_verify(&spec_bounded.expr);
294
295    // 2. Translate SVA with G-wrapping to match FOL temporal structure
296    let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
297    let sva_verify = bounded_to_verify(&sva_bounded.expr);
298
299    // 3. Signal list from declarations + any extra signals from either side
300    let mut all_signals: Vec<String> = decls.iter().map(|d| d.sva_name.clone()).collect();
301    let spec_signals = extract_signal_names(&spec_bounded);
302    let sva_signals = extract_signal_names(&sva_bounded);
303    for sig in spec_signals.into_iter().chain(sva_signals.into_iter()) {
304        if !all_signals.contains(&sig) {
305            all_signals.push(sig);
306        }
307    }
308
309    // 4. Z3 equivalence check
310    Ok(logicaffeine_verify::equivalence::check_equivalence(
311        &spec_verify, &sva_verify, &all_signals, bound as usize,
312    ))
313}
314
315/// Check FOL ↔ SVA **semantic** equivalence at a bound with our pure-Rust, certified prover
316/// (no Z3) — the in-browser counterpart of `check_z3_equivalence`. Lowers both bounded
317/// obligations to `ProofExpr` and discharges `F ↔ S` through the CDCL → RUP tiers: an
318/// `equivalent` verdict is RUP-certified, a non-equivalent one carries a counterexample
319/// trace (`name@t` → `"1"`/`"0"`). Errors (fail-closed, never a false "equivalent") if
320/// either obligation leaves the Boolean fragment — data-path needs bit-blasting, not wired
321/// yet.
322pub fn prove_bounded_equivalence(
323    fol_bounded: &BoundedExpr,
324    sva_bounded: &BoundedExpr,
325    bound: u32,
326) -> Result<EquivalenceResult, HwError> {
327    use super::sva_to_proof::bounded_to_proof;
328    use logicaffeine_proof::sat::{prove_equivalence, EquivOutcome};
329
330    let fol = bounded_to_proof(fol_bounded).ok_or_else(|| {
331        HwError::VerificationError(
332            "FOL obligation is outside the Boolean fragment (bit-blasting not yet wired)".into(),
333        )
334    })?;
335    let sva = bounded_to_proof(sva_bounded).ok_or_else(|| {
336        HwError::VerificationError(
337            "SVA obligation is outside the Boolean fragment (bit-blasting not yet wired)".into(),
338        )
339    })?;
340
341    match prove_equivalence(&fol, &sva) {
342        EquivOutcome::Equivalent => Ok(EquivalenceResult {
343            equivalent: true,
344            counterexample: None,
345            bound,
346        }),
347        EquivOutcome::Differ(model) => Ok(EquivalenceResult {
348            equivalent: false,
349            counterexample: Some(
350                model
351                    .into_iter()
352                    .map(|(name, v)| (name, if v { "1" } else { "0" }.to_string()))
353                    .collect(),
354            ),
355            bound,
356        }),
357        EquivOutcome::Unsupported => Err(HwError::VerificationError(
358            "obligation not purely propositional — escalate to bit-blasting".into(),
359        )),
360    }
361}
362
363/// End-to-end: an English spec and a candidate SVA string → certified semantic equivalence
364/// with our prover. The Z3-free counterpart of `check_z3_equivalence`: the same
365/// translators, our CDCL → RUP tiers instead of Z3.
366pub fn prove_spec_sva_equivalence(
367    spec_source: &str,
368    sva_text: &str,
369    bound: u32,
370) -> Result<EquivalenceResult, HwError> {
371    let spec_bounded = translate_spec_to_bounded(spec_source, bound)?;
372    let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
373    prove_bounded_equivalence(&spec_bounded.expr, &sva_bounded.expr, bound)
374}
375
376/// A node in the renderable knowledge-graph view: a signal with its role and width.
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct KgNode {
379    pub name: String,
380    pub role: String,
381    pub width: u32,
382}
383
384/// A directed relation between two nodes (indices into [`KgSummary::nodes`]).
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct KgLink {
387    pub from: usize,
388    pub to: usize,
389    pub relation: String,
390}
391
392/// A compact, render-ready view of the hardware knowledge graph extracted from an English
393/// spec — signals as nodes, relations (drives / triggers / handshakes / …) as directed links.
394#[derive(Debug, Clone, PartialEq, Eq, Default)]
395pub struct KgSummary {
396    pub nodes: Vec<KgNode>,
397    pub links: Vec<KgLink>,
398}
399
400/// Extract a render-ready knowledge graph from an English hardware spec. Pure Rust (no Z3),
401/// so it runs in the browser. Merges the legacy and typed (ontology) edge sets.
402pub fn kg_summary(spec: &str) -> Result<KgSummary, HwError> {
403    use logicaffeine_language::semantics::knowledge_graph::{KgRelation, SignalRole};
404    use std::collections::HashMap;
405
406    let kg = extract_kg(spec)?;
407    let mut nodes = Vec::new();
408    let mut index: HashMap<String, usize> = HashMap::new();
409    for s in &kg.signals {
410        let role = match s.role {
411            SignalRole::Input => "input",
412            SignalRole::Output => "output",
413            SignalRole::Internal => "internal",
414            SignalRole::Clock => "clock",
415        }
416        .to_string();
417        index.insert(s.name.clone(), nodes.len());
418        nodes.push(KgNode { name: s.name.clone(), role, width: s.width });
419    }
420
421    let mut links = Vec::new();
422    let mut seen = std::collections::HashSet::new();
423    let mut add = |from: &str, to: &str, rel: String, links: &mut Vec<KgLink>| {
424        if let (Some(&f), Some(&t)) = (index.get(from), index.get(to)) {
425            if f != t && seen.insert((f, t, rel.clone())) {
426                links.push(KgLink { from: f, to: t, relation: rel });
427            }
428        }
429    };
430    for e in &kg.edges {
431        let rel = match e.relation {
432            KgRelation::Temporal => "temporal",
433            KgRelation::Triggers => "triggers",
434            KgRelation::Constrains => "constrains",
435            KgRelation::TypeOf => "type",
436        }
437        .to_string();
438        add(&e.from, &e.to, rel, &mut links);
439    }
440    for (from, to, rel) in &kg.typed_edges {
441        add(from, to, typed_relation_label(rel), &mut links);
442    }
443
444    Ok(KgSummary { nodes, links })
445}
446
447fn typed_relation_label(rel: &logicaffeine_language::semantics::knowledge_graph::HwRelation) -> String {
448    use logicaffeine_language::semantics::knowledge_graph::HwRelation as R;
449    match rel {
450        R::Drives | R::DrivesRegistered { .. } => "drives",
451        R::DataFlow => "data",
452        R::Reads => "reads",
453        R::Writes => "writes",
454        R::Controls => "controls",
455        R::Selects => "selects",
456        R::Enables => "enables",
457        R::Resets => "resets",
458        R::Triggers { .. } => "triggers",
459        R::Constrains => "constrains",
460        R::Follows { .. } => "follows",
461        R::Precedes => "precedes",
462        R::Preserves => "preserves",
463        R::Contains => "contains",
464        R::Instantiates => "instantiates",
465        R::ConnectsTo => "connects",
466        R::BelongsToDomain { .. } => "domain",
467        R::HandshakesWith => "handshake",
468        R::Acknowledges => "acks",
469        R::Pipelines { .. } => "pipeline",
470        R::MutuallyExcludes => "mutex",
471        R::EventuallyFollows => "eventually",
472        R::AssumedBy => "assumed",
473    }
474    .to_string()
475}
476
477/// One signal's value over time in a counterexample waveform. `width` is the bit width
478/// (1 = a boolean control bit); each `values[t]` is the (reconstructed) register value at
479/// timestep `t`, or `None` if unconstrained there.
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct WaveSignal {
482    pub name: String,
483    pub width: u32,
484    pub values: Vec<Option<u64>>,
485}
486
487/// A counterexample rendered as a waveform: signals (rows) over discrete timesteps (columns).
488#[derive(Debug, Clone, PartialEq, Eq, Default)]
489pub struct Waveform {
490    pub timesteps: u32,
491    pub signals: Vec<WaveSignal>,
492}
493
494/// Turn a counterexample's bindings into a waveform. Handles both 1-bit signals (`name@t`)
495/// and bit-blasted multi-bit registers (`name@t#i`), reconstructing each register's integer
496/// value per timestep from its bits. Signals are sorted by name; each row spans
497/// `0..timesteps`. Malformed keys (no `@t`) are skipped.
498pub fn counterexample_waveform(counterexample: &[(String, String)]) -> Waveform {
499    use std::collections::BTreeMap;
500    // name → (width, timestep → (bit-index → value))
501    struct Acc {
502        width: u32,
503        per_t: BTreeMap<u32, BTreeMap<u32, bool>>,
504    }
505    let mut sigs: BTreeMap<String, Acc> = BTreeMap::new();
506    let mut max_t = 0u32;
507    let mut any = false;
508    for (key, val) in counterexample {
509        let bit_val = match val.as_str() {
510            "1" | "true" => true,
511            "0" | "false" => false,
512            _ => continue,
513        };
514        // Optional `#<bit>` suffix marks a bit of a bit-blasted register.
515        let (base, bit, multibit) = match key.rsplit_once('#') {
516            Some((b, idx)) => match idx.parse::<u32>() {
517                Ok(i) => (b, i, true),
518                Err(_) => continue,
519            },
520            None => (key.as_str(), 0u32, false),
521        };
522        let Some((name, t_str)) = base.rsplit_once('@') else {
523            continue;
524        };
525        let Ok(t) = t_str.parse::<u32>() else {
526            continue;
527        };
528        let acc = sigs.entry(name.to_string()).or_insert(Acc { width: 1, per_t: BTreeMap::new() });
529        if multibit {
530            acc.width = acc.width.max(bit + 1);
531        }
532        acc.per_t.entry(t).or_default().insert(bit, bit_val);
533        max_t = max_t.max(t);
534        any = true;
535    }
536    let timesteps = if any { max_t + 1 } else { 0 };
537    let signals = sigs
538        .into_iter()
539        .map(|(name, acc)| {
540            let values = (0..timesteps)
541                .map(|t| {
542                    acc.per_t.get(&t).map(|bits| {
543                        bits.iter().fold(0u64, |v, (&i, &b)| if b { v | (1u64 << i) } else { v })
544                    })
545                })
546                .collect();
547            WaveSignal { name, width: acc.width, values }
548        })
549        .collect();
550    Waveform { timesteps, signals }
551}
552
553/// A vacuity verdict for a synthesized property.
554#[derive(Debug, Clone, PartialEq, Eq)]
555pub enum VacuityReport {
556    /// The trigger (implication antecedent) is reachable — the property is non-vacuous.
557    NonVacuous,
558    /// The trigger can never fire — the property holds vacuously (a dead trigger / likely bug).
559    Vacuous,
560    /// The property has no antecedent (e.g. a pure safety assertion) — vacuity does not apply.
561    NoTrigger,
562    /// The antecedent left the Boolean fragment — escalate (bit-blasting).
563    Unsupported,
564}
565
566/// The first implication antecedent reachable through the Boolean structure of a bounded
567/// property — the SVA "trigger". `None` for a property with no implication (pure safety).
568fn first_antecedent(e: &BoundedExpr) -> Option<&BoundedExpr> {
569    match e {
570        BoundedExpr::Implies(a, _) => Some(a),
571        BoundedExpr::And(l, r) | BoundedExpr::Or(l, r) => {
572            first_antecedent(l).or_else(|| first_antecedent(r))
573        }
574        BoundedExpr::Not(x) => first_antecedent(x),
575        _ => None,
576    }
577}
578
579/// Vacuity check (Z3-free, certified): does the property's trigger ever fire? An
580/// unsatisfiable antecedent means the property passes vacuously — a dead trigger that usually
581/// signals a malformed spec. Reuses [`logicaffeine_proof::bmc::check_vacuity`].
582pub fn check_spec_vacuity(spec_source: &str, bound: u32) -> Result<VacuityReport, HwError> {
583    use super::sva_to_proof::bounded_to_proof;
584    use logicaffeine_proof::bmc::{check_vacuity, VacuityOutcome};
585
586    let fol = translate_spec_to_bounded(spec_source, bound)?;
587    let antecedent = match first_antecedent(&fol.expr) {
588        Some(a) => a,
589        None => return Ok(VacuityReport::NoTrigger),
590    };
591    let proof = match bounded_to_proof(antecedent) {
592        Some(p) => p,
593        None => return Ok(VacuityReport::Unsupported),
594    };
595    Ok(match check_vacuity(&proof) {
596        VacuityOutcome::Vacuous => VacuityReport::Vacuous,
597        VacuityOutcome::Reachable(_) => VacuityReport::NonVacuous,
598        VacuityOutcome::Unsupported => VacuityReport::Unsupported,
599    })
600}
601
602#[cfg(test)]
603mod native_prove_tests {
604    use super::*;
605
606    /// A property is equivalent to itself — the load-bearing identity, RUP-certified.
607    #[test]
608    fn sva_self_equivalence_is_certified() {
609        let a = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
610        let b = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
611        let r = prove_bounded_equivalence(&a.expr, &b.expr, 3).unwrap();
612        assert!(r.equivalent, "self-equivalence must hold");
613        assert!(r.counterexample.is_none());
614    }
615
616    /// The killer case: De Morgan duals are structurally DIFFERENT but semantically EQUAL.
617    /// Structural equality (`check_bounded_equivalence`) cannot see it; our certified SAT
618    /// prover can. This is the whole point of semantic equivalence.
619    #[test]
620    fn de_morgan_is_semantically_equivalent() {
621        let a = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
622        let b = translate_sva_to_bounded("!grant_a || !grant_b", 3).unwrap();
623        assert!(
624            !check_bounded_equivalence(&a.expr, &b.expr, 3).equivalent,
625            "structural equality should NOT see De Morgan duals as equal"
626        );
627        let r = prove_bounded_equivalence(&a.expr, &b.expr, 3).unwrap();
628        assert!(r.equivalent, "De Morgan duals must be semantically equivalent");
629    }
630
631    /// End-to-end: synthesizing SVA from an English spec must PRESERVE meaning — the
632    /// synthesized SVA is certified equivalent to the spec's own FOL, by our prover.
633    #[test]
634    fn synthesized_sva_is_equivalent_to_its_spec() {
635        let spec = "Always, if request is high, then acknowledge is high.";
636        let synth = crate::codegen_sva::fol_to_sva::synthesize_sva_from_spec(spec, "clk").unwrap();
637        let r = prove_spec_sva_equivalence(spec, &synth.body, 3).unwrap();
638        assert!(r.equivalent, "synthesis must preserve meaning; got {:?}", r);
639    }
640
641    /// Distinct properties differ, and we recover a concrete counterexample trace.
642    #[test]
643    fn distinct_properties_differ_with_counterexample() {
644        let a = translate_sva_to_bounded("!(grant_a && grant_b)", 2).unwrap();
645        let b = translate_sva_to_bounded("grant_a |-> grant_b", 2).unwrap();
646        let r = prove_bounded_equivalence(&a.expr, &b.expr, 2).unwrap();
647        assert!(!r.equivalent, "a mutex and an implication are not equivalent");
648        let ce = r.counterexample.expect("a counterexample trace");
649        assert!(!ce.is_empty(), "counterexample must bind some signal@t");
650    }
651
652    #[test]
653    fn implication_spec_has_a_reachable_trigger() {
654        // The antecedent (`request is high`) is over a free signal, so it is reachable —
655        // the property is non-vacuous.
656        let r = check_spec_vacuity("Always, if request is high, then acknowledge is high.", 3)
657            .unwrap();
658        assert_eq!(r, VacuityReport::NonVacuous);
659    }
660
661    #[test]
662    fn kg_summary_extracts_signals_and_relations() {
663        let kg = kg_summary("Always, if request is high, then acknowledge is high.")
664            .expect("extracts a knowledge graph");
665        assert!(!kg.nodes.is_empty(), "the spec mentions signals: {kg:?}");
666        // Every link must reference valid node indices.
667        for l in &kg.links {
668            assert!(l.from < kg.nodes.len() && l.to < kg.nodes.len(), "dangling link {l:?}");
669            assert!(!l.relation.is_empty());
670        }
671        // The named signals should appear (proper nouns are capitalized in the KG).
672        let names: Vec<&str> = kg.nodes.iter().map(|n| n.name.as_str()).collect();
673        assert!(
674            names.iter().any(|n| n.eq_ignore_ascii_case("request")),
675            "expected a Request signal among {names:?}"
676        );
677        assert!(
678            names.iter().any(|n| n.eq_ignore_ascii_case("acknowledge")),
679            "expected an Acknowledge signal among {names:?}"
680        );
681    }
682
683    #[test]
684    fn waveform_groups_boolean_signals_over_timesteps() {
685        let ce = vec![
686            ("req@0".to_string(), "0".to_string()),
687            ("req@1".to_string(), "1".to_string()),
688            ("ack@1".to_string(), "0".to_string()),
689        ];
690        let wf = counterexample_waveform(&ce);
691        assert_eq!(wf.timesteps, 2);
692        assert_eq!(wf.signals.len(), 2);
693        let ack = wf.signals.iter().find(|s| s.name == "ack").unwrap();
694        assert_eq!(ack.width, 1);
695        assert_eq!(ack.values, vec![None, Some(0)]);
696        let req = wf.signals.iter().find(|s| s.name == "req").unwrap();
697        assert_eq!(req.values, vec![Some(0), Some(1)]);
698    }
699
700    #[test]
701    fn waveform_reconstructs_multibit_register_values() {
702        // cnt is 2 bits: at t0 = 0b01 = 1, at t1 = 0b10 = 2.
703        let ce = vec![
704            ("cnt@0#0".to_string(), "1".to_string()),
705            ("cnt@0#1".to_string(), "0".to_string()),
706            ("cnt@1#0".to_string(), "0".to_string()),
707            ("cnt@1#1".to_string(), "1".to_string()),
708        ];
709        let wf = counterexample_waveform(&ce);
710        let cnt = wf.signals.iter().find(|s| s.name == "cnt").unwrap();
711        assert_eq!(cnt.width, 2);
712        assert_eq!(cnt.values, vec![Some(1), Some(2)]);
713    }
714
715    #[test]
716    fn waveform_skips_malformed_keys() {
717        let ce = vec![("nonsense".to_string(), "1".to_string())];
718        assert_eq!(counterexample_waveform(&ce), Waveform::default());
719    }
720
721    #[test]
722    fn first_antecedent_extracts_the_trigger() {
723        use crate::codegen_sva::sva_to_verify::BoundedExpr;
724        let v = |s: &str| BoundedExpr::Var(s.to_string());
725        let bx = |e: BoundedExpr| Box::new(e);
726        // ⋀ over timesteps of (req@t → ack@t): the first antecedent is req@0.
727        let prop = BoundedExpr::And(
728            bx(BoundedExpr::Implies(bx(v("req@0")), bx(v("ack@0")))),
729            bx(BoundedExpr::Implies(bx(v("req@1")), bx(v("ack@1")))),
730        );
731        assert_eq!(first_antecedent(&prop), Some(&v("req@0")));
732        // A pure safety property (no implication) has no trigger.
733        let safety = BoundedExpr::Not(bx(BoundedExpr::And(bx(v("a@0")), bx(v("b@0")))));
734        assert_eq!(first_antecedent(&safety), None);
735    }
736}
737
738/// Check if two BoundedExpr trees are structurally equal.
739fn bounded_exprs_equal(a: &BoundedExpr, b: &BoundedExpr) -> bool {
740    match (a, b) {
741        (BoundedExpr::Var(va), BoundedExpr::Var(vb)) => va == vb,
742        (BoundedExpr::Bool(va), BoundedExpr::Bool(vb)) => va == vb,
743        (BoundedExpr::Int(va), BoundedExpr::Int(vb)) => va == vb,
744        (BoundedExpr::And(la, ra), BoundedExpr::And(lb, rb)) => {
745            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
746        }
747        (BoundedExpr::Or(la, ra), BoundedExpr::Or(lb, rb)) => {
748            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
749        }
750        (BoundedExpr::Not(ia), BoundedExpr::Not(ib)) => bounded_exprs_equal(ia, ib),
751        (BoundedExpr::Implies(la, ra), BoundedExpr::Implies(lb, rb)) => {
752            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
753        }
754        (BoundedExpr::Eq(la, ra), BoundedExpr::Eq(lb, rb)) => {
755            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
756        }
757        (BoundedExpr::Lt(la, ra), BoundedExpr::Lt(lb, rb)) => {
758            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
759        }
760        (BoundedExpr::Gt(la, ra), BoundedExpr::Gt(lb, rb)) => {
761            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
762        }
763        (BoundedExpr::Lte(la, ra), BoundedExpr::Lte(lb, rb)) => {
764            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
765        }
766        (BoundedExpr::Gte(la, ra), BoundedExpr::Gte(lb, rb)) => {
767            bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
768        }
769        _ => false,
770    }
771}