Skip to main content

logicaffeine_compile/codegen_sva/
fol_to_sva.rs

1//! FOL → SVA Formal Synthesis
2//!
3//! Pattern-matches Kripke-lowered FOL structures to synthesize
4//! SystemVerilog Assertions. The key patterns:
5//!
6//! | Kripke Pattern | SVA Output |
7//! |---|---|
8//! | `∀w'(Accessible_Temporal → P(w'))` | `assert property(@(posedge clk) P)` |
9//! | `∃w'(Reachable_Temporal ∧ P(w'))` | `cover property(s_eventually(P))` |
10//! | `∀w'(Next_Temporal → P(w'))` | `nexttime(P)` |
11//! | User `If`: `P → Q` with worlds | `P \|-> Q` |
12//! | `¬(P ∧ Q)` with worlds | `!(P && Q)` |
13
14use logicaffeine_language::ast::logic::{LogicExpr, QuantifierKind, TemporalOperator, ThematicRole, Term};
15use logicaffeine_language::token::TokenType;
16use logicaffeine_language::Interner;
17
18/// Result of SVA synthesis from a specification.
19#[derive(Debug)]
20pub struct SynthesizedSva {
21    /// Full SVA text including property wrapper and clock.
22    pub sva_text: String,
23    /// The SVA body expression (without property/assert wrapper).
24    pub body: String,
25    /// Signal names extracted from the specification.
26    pub signals: Vec<String>,
27    /// The assertion kind (assert/cover/assume).
28    pub kind: String,
29}
30
31/// Synthesize an SVA property from an English specification.
32///
33/// Parses the spec, applies Kripke lowering, then pattern-matches the
34/// resulting FOL structure to produce SVA. The synthesized SVA uses the
35/// EXACT same signal names as the FOL translator so Z3 equivalence checking works.
36pub fn synthesize_sva_from_spec(spec: &str, clock: &str) -> Result<SynthesizedSva, String> {
37    // Literate / multi-section content (the Logic editor, `## Theorem` blocks,
38    // `## Hardware` sections, …) cannot be parsed mid-stream by the property parser,
39    // which only consumes a single property and chokes on a block header. Split the
40    // spec at block headers and synthesize from the FIRST section that yields a real
41    // property, so a leading or trailing theorem/main block no longer breaks
42    // "Compile to SVA".
43    let mut last_err: Option<String> = None;
44    for block in spec_blocks(spec) {
45        if block.trim().is_empty() {
46            continue;
47        }
48        match synthesize_one(&block, clock) {
49            Ok(s) => return Ok(s),
50            Err(e) => last_err = Some(e),
51        }
52    }
53    Err(last_err.unwrap_or_else(|| {
54        "No hardware property found. Hardware specs are temporal sentences like \
55         \"Always, if request is high, then grant is high.\""
56            .to_string()
57    }))
58}
59
60/// Split a spec into content blocks at literate block headers (`## …` lines), dropping
61/// the header lines themselves. A spec with no headers yields a single block (itself).
62fn spec_blocks(spec: &str) -> Vec<String> {
63    let mut blocks = Vec::new();
64    let mut cur = String::new();
65    for line in spec.lines() {
66        if line.trim_start().starts_with("##") {
67            if !cur.trim().is_empty() {
68                blocks.push(std::mem::take(&mut cur));
69            }
70            cur.clear();
71        } else {
72            cur.push_str(line);
73            cur.push('\n');
74        }
75    }
76    if !cur.trim().is_empty() {
77        blocks.push(cur);
78    }
79    if blocks.is_empty() {
80        blocks.push(spec.to_string());
81    }
82    blocks
83}
84
85fn synthesize_one(spec: &str, clock: &str) -> Result<SynthesizedSva, String> {
86    use logicaffeine_language::compile_kripke_with;
87    use logicaffeine_language::semantics::knowledge_graph::extract_from_kripke_ast;
88    use super::fol_to_verify::FolTranslator;
89    use super::sva_to_verify::extract_signal_names;
90
91    // Parse and Kripke-lower the spec, then extract BOTH the SVA body
92    // AND the FOL signal names (so they match for Z3 equivalence)
93    let (sva_body, signals, fol_signals) = compile_kripke_with(spec, |ast, interner| {
94        // Get the FOL translator's signal names
95        let mut fol_translator = FolTranslator::new(interner, 5);
96        let fol_result = fol_translator.translate_property(ast);
97        let fol_sigs = extract_signal_names(&fol_result);
98
99        // Get KG signals for metadata
100        let kg = extract_from_kripke_ast(ast, interner);
101        let kg_signals: Vec<String> = kg.signals.iter().map(|s| s.name.clone()).collect();
102
103        // Synthesize SVA body using signal names from the FOL translator
104        let body = synthesize_from_ast(ast, interner, clock, &fol_sigs);
105        (body, kg_signals, fol_sigs)
106    }).map_err(|_e| {
107        "not a hardware property — I couldn't read a temporal spec here. Try a sentence like \
108         \"Always, if request is high, then grant is high.\""
109            .to_string()
110    })?;
111
112    let body = sva_body;
113
114    // Reject degenerate synthesis results — these indicate the spec is not
115    // a temporal property (e.g., bare action sentences like "The bus acknowledges the request.")
116    if body.trim() == "0" {
117        return Err("Not a temporal property: this sentence describes an action or event, \
118            not a verifiable hardware property. Wrap in a temporal operator \
119            (e.g., \"Always, ...\") or restructure as a conditional.".to_string());
120    }
121
122    // Determine the assertion kind from the property's SHAPE, not a substring search. A `cover`
123    // only witnesses that a scenario is reachable; an `assert` checks a property always holds. So
124    // a `cover` is justified ONLY for a top-level reachability claim (a bare `s_eventually`/`cover`
125    // with no implication guarding it) — a liveness implication like `req |-> s_eventually(grant)`
126    // is an ASSERTION, not a cover.
127    let trimmed = body.trim_start();
128    let is_reachability_cover = (trimmed.starts_with("s_eventually(") || trimmed.starts_with("cover"))
129        && !body.contains("|->")
130        && !body.contains("|=>");
131    let kind = if is_reachability_cover { "cover" } else { "assert" };
132
133    let sva_text = format!(
134        "{} property (@(posedge {}) {});",
135        kind, clock, body
136    );
137
138    Ok(SynthesizedSva {
139        sva_text,
140        body,
141        signals: if signals.is_empty() { fol_signals } else { signals },
142        kind: kind.to_string(),
143    })
144}
145
146/// Synthesize SVA body from a Kripke-lowered AST node.
147/// Uses `fol_signals` (the signal names the FOL translator produces) to ensure
148/// the synthesized SVA uses matching variable names for Z3 equivalence.
149fn synthesize_from_ast<'a>(
150    expr: &'a LogicExpr<'a>,
151    interner: &Interner,
152    clock: &str,
153    fol_signals: &[String],
154) -> String {
155    match expr {
156        // Temporal unary: G(P) → P, F(P) → s_eventually(P), X(P) → nexttime(P)
157        LogicExpr::Temporal { operator, body } => {
158            let inner = synthesize_from_ast(body, interner, clock, fol_signals);
159            match operator {
160                TemporalOperator::Always => inner, // G is implicit in assert property
161                TemporalOperator::Eventually => format!("s_eventually({})", inner),
162                TemporalOperator::Next => format!("nexttime({})", inner),
163                TemporalOperator::BoundedEventually(n) => format!("##[0:{}] {}", n, inner),
164                _ => inner,
165            }
166        }
167
168        // Kripke-lowered G: ∀w'(Accessible_Temporal(w,w') → P(w'))
169        // Kripke-lowered X: ∀w'(Next_Temporal(w,w') → P(w'))
170        LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, variable, .. } => {
171            let var_name = interner.resolve(*variable).to_string();
172            if var_name.starts_with('w') {
173                if let LogicExpr::BinaryOp { left, right, op: TokenType::Implies } = body {
174                    if is_accessibility_predicate(left, interner) {
175                        let inner = synthesize_from_ast(right, interner, clock, fol_signals);
176                        // Distinguish Next_Temporal → nexttime(P) vs Accessible → P
177                        if is_next_temporal_predicate(left, interner) {
178                            return format!("nexttime({})", inner);
179                        }
180                        return inner;
181                    }
182                }
183            }
184            // Regular quantifier — synthesize body
185            synthesize_from_ast(body, interner, clock, fol_signals)
186        }
187
188        // Kripke-lowered F: ∃w'(Reachable_Temporal(w,w') ∧ P(w'))
189        LogicExpr::Quantifier { kind: QuantifierKind::Existential, body, variable, .. } => {
190            let var_name = interner.resolve(*variable).to_string();
191            if var_name.starts_with('w') {
192                if let LogicExpr::BinaryOp { left, right, op: TokenType::And } = body {
193                    if is_accessibility_predicate(left, interner) {
194                        return format!("s_eventually({})", synthesize_from_ast(right, interner, clock, fol_signals));
195                    }
196                }
197            }
198            synthesize_from_ast(body, interner, clock, fol_signals)
199        }
200
201        // Counting quantifiers: AtMost(n), AtLeast(n), Cardinal(n)
202        LogicExpr::Quantifier { kind: QuantifierKind::AtMost(n), body, .. } => {
203            let inner = synthesize_from_ast(body, interner, clock, fol_signals);
204            if *n == 1 {
205                format!("$onehot0({})", inner)
206            } else {
207                format!("($countones({}) <= {})", inner, n)
208            }
209        }
210
211        LogicExpr::Quantifier { kind: QuantifierKind::AtLeast(n), body, .. } => {
212            let inner = synthesize_from_ast(body, interner, clock, fol_signals);
213            if *n == 1 {
214                inner // at least one → signal is high (OR-reduction implicit)
215            } else {
216                format!("($countones({}) >= {})", inner, n)
217            }
218        }
219
220        LogicExpr::Quantifier { kind: QuantifierKind::Cardinal(n), body, .. } => {
221            let inner = synthesize_from_ast(body, interner, clock, fol_signals);
222            if *n == 1 {
223                format!("$onehot({})", inner)
224            } else {
225                format!("($countones({}) == {})", inner, n)
226            }
227        }
228
229        // Other quantifier kinds (Most, Few, Many, Generic) — synthesize body
230        LogicExpr::Quantifier { body, .. } => {
231            synthesize_from_ast(body, interner, clock, fol_signals)
232        }
233
234        // User conditional: P → Q (TokenType::If from parser)
235        LogicExpr::BinaryOp { left, right, op: TokenType::If } => {
236            let ante = synthesize_from_ast(left, interner, clock, fol_signals);
237            let cons = synthesize_from_ast(right, interner, clock, fol_signals);
238            format!("{} |-> {}", ante, cons)
239        }
240
241        // Compiler-generated implication (restriction): synthesize as SVA implication
242        // ∀x(Restriction(x) → Body(x)) → restriction |-> body
243        // This preserves the full semantic content for Z3 equivalence checking.
244        LogicExpr::BinaryOp { left, right, op: TokenType::Implies } => {
245            let ante = synthesize_from_ast(left, interner, clock, fol_signals);
246            let cons = synthesize_from_ast(right, interner, clock, fol_signals);
247            // If the antecedent is just "1" (vacuous), skip the implication
248            if ante == "1" {
249                cons
250            } else {
251                format!("(!({}) || ({}))", ante, cons)
252            }
253        }
254
255        // Conjunction
256        LogicExpr::BinaryOp { left, right, op: TokenType::And } => {
257            let l = synthesize_from_ast(left, interner, clock, fol_signals);
258            let r = synthesize_from_ast(right, interner, clock, fol_signals);
259            format!("({} && {})", l, r)
260        }
261
262        // Disjunction
263        LogicExpr::BinaryOp { left, right, op: TokenType::Or } => {
264            let l = synthesize_from_ast(left, interner, clock, fol_signals);
265            let r = synthesize_from_ast(right, interner, clock, fol_signals);
266            format!("({} || {})", l, r)
267        }
268
269        // Negation
270        LogicExpr::UnaryOp { operand, .. } => {
271            let inner = synthesize_from_ast(operand, interner, clock, fol_signals);
272            format!("!({})", inner)
273        }
274
275        // Predicate: map to the FOL signal name so Z3 sees matching variables
276        LogicExpr::Predicate { name, args, .. } => {
277            let pred_name = interner.resolve(*name).to_string();
278            // Skip meta-predicates
279            if pred_name.contains("Accessible") || pred_name.contains("Reachable")
280                || pred_name.contains("Next_Temporal")
281                || pred_name == "Agent" || pred_name == "Theme"
282            {
283                return "1".to_string(); // vacuously true
284            }
285            // Build precise candidate: PredName_argName_ (matches FolTranslator naming)
286            let arg_name = args.first().map(|a| term_to_string_helper(a, interner));
287            if let Some(ref arg) = arg_name {
288                let candidate = format!("{}_{}_", pred_name, arg);
289                if let Some(fol_sig) = fol_signals.iter().find(|s| {
290                    s.to_lowercase() == candidate.to_lowercase()
291                }) {
292                    return fol_sig.clone();
293                }
294            }
295            // Fallback: fuzzy match on predicate name
296            if let Some(fol_sig) = fol_signals.iter().find(|s| {
297                let s_lower = s.to_lowercase();
298                s_lower.contains(&pred_name.to_lowercase())
299                    || pred_name.to_lowercase().contains(&s_lower)
300            }) {
301                fol_sig.clone()
302            } else {
303                pred_name.to_lowercase()
304            }
305        }
306
307        // NeoEvent: extract verb + agent as signal (matching FolTranslator naming)
308        LogicExpr::NeoEvent(data) => {
309            let verb_name = interner.resolve(data.verb).to_string();
310            let agent_name = data.roles.iter()
311                .find(|(role, _)| matches!(role, ThematicRole::Agent))
312                .map(|(_, term)| term_to_string_helper(term, interner));
313
314            let candidate = if let Some(ref arg) = agent_name {
315                format!("{}_{}_", verb_name, arg)
316            } else {
317                verb_name.clone()
318            };
319
320            // Match against fol_signals for consistency
321            if let Some(fol_sig) = fol_signals.iter().find(|s| {
322                s.to_lowercase() == candidate.to_lowercase()
323            }) {
324                fol_sig.clone()
325            } else if let Some(fol_sig) = fol_signals.iter().find(|s| {
326                let s_lower = s.to_lowercase();
327                s_lower.contains(&verb_name.to_lowercase())
328            }) {
329                fol_sig.clone()
330            } else {
331                candidate
332            }
333        }
334
335        // Temporal binary
336        LogicExpr::TemporalBinary { operator, left, right } => {
337            let l = synthesize_from_ast(left, interner, clock, fol_signals);
338            let r = synthesize_from_ast(right, interner, clock, fol_signals);
339            use logicaffeine_language::ast::logic::BinaryTemporalOp;
340            match operator {
341                BinaryTemporalOp::Until => format!("({} until {})", l, r),
342                BinaryTemporalOp::Release => format!("({} release {})", l, r),
343                BinaryTemporalOp::WeakUntil => format!("({} weak_until {})", l, r),
344            }
345        }
346
347        // Modal: unwrap
348        LogicExpr::Modal { operand, .. } => {
349            synthesize_from_ast(operand, interner, clock, fol_signals)
350        }
351
352        // Aspectual: HAB(P), PROG(P), PERF(P), ITER(P) → unwrap to body
353        // In hardware context, habitual aspect means "P holds generally"
354        LogicExpr::Aspectual { body, .. } => {
355            synthesize_from_ast(body, interner, clock, fol_signals)
356        }
357
358        // Voice: PASSIVE(P) → unwrap to body
359        LogicExpr::Voice { body, .. } => {
360            synthesize_from_ast(body, interner, clock, fol_signals)
361        }
362
363        // Relation: S-V-O → map verb and subject/object to signal names
364        LogicExpr::Relation(data) => {
365            let verb_name = interner.resolve(data.verb).to_string();
366            let subj_name = interner.resolve(data.subject.noun).to_string();
367            let obj_name = interner.resolve(data.object.noun).to_string();
368            let candidate = format!("{}_{}_", verb_name, subj_name);
369            if let Some(fol_sig) = fol_signals.iter().find(|s| {
370                s.to_lowercase() == candidate.to_lowercase()
371            }) {
372                fol_sig.clone()
373            } else if let Some(fol_sig) = fol_signals.iter().find(|s| {
374                let s_lower = s.to_lowercase();
375                s_lower.contains(&verb_name.to_lowercase())
376                    || s_lower.contains(&subj_name.to_lowercase())
377                    || s_lower.contains(&obj_name.to_lowercase())
378            }) {
379                fol_sig.clone()
380            } else {
381                format!("{}_{}_", verb_name, obj_name).to_lowercase()
382            }
383        }
384
385        // Categorical: Aristotelian A/E/I/O → synthesize subject and predicate
386        LogicExpr::Categorical(data) => {
387            let subj_name = interner.resolve(data.subject.noun).to_string().to_lowercase();
388            let pred_name = interner.resolve(data.predicate.noun).to_string().to_lowercase();
389            if data.copula_negative {
390                format!("({} && !({}))", subj_name, pred_name)
391            } else {
392                format!("(!({}) || ({}))", subj_name, pred_name)
393            }
394        }
395
396        // Scopal: "only X", "always X" as scopal adverb → unwrap to body
397        LogicExpr::Scopal { body, .. } => {
398            synthesize_from_ast(body, interner, clock, fol_signals)
399        }
400
401        // Causal: "effect because cause" → both sides as conjunction
402        LogicExpr::Causal { effect, cause } => {
403            let e = synthesize_from_ast(effect, interner, clock, fol_signals);
404            let c = synthesize_from_ast(cause, interner, clock, fol_signals);
405            format!("({} && {})", c, e)
406        }
407
408        // Concessive: "main, although concession" → the main clause is asserted.
409        LogicExpr::Concessive { main, .. } => {
410            synthesize_from_ast(main, interner, clock, fol_signals)
411        }
412
413        // Atom: bare symbol → treat as signal name
414        LogicExpr::Atom(sym) => {
415            let name = interner.resolve(*sym).to_string();
416            if let Some(fol_sig) = fol_signals.iter().find(|s| {
417                s.to_lowercase() == name.to_lowercase()
418            }) {
419                fol_sig.clone()
420            } else {
421                name.to_lowercase()
422            }
423        }
424
425        // Identity: t1 = t2 → equality check
426        LogicExpr::Identity { left, right } => {
427            let l = term_to_string_helper(left, interner).to_lowercase();
428            let r = term_to_string_helper(right, interner).to_lowercase();
429            format!("({} == {})", l, r)
430        }
431
432        // Default: fail closed. Unhandled FOL patterns must NOT silently
433        // become vacuously true in synthesized SVA (Sprint 0A consistency).
434        _ => "0".to_string(),
435    }
436}
437
438/// Check if an expression is an accessibility predicate (Accessible_Temporal, Reachable_Temporal, etc.).
439fn is_accessibility_predicate<'a>(expr: &'a LogicExpr<'a>, interner: &Interner) -> bool {
440    if let LogicExpr::Predicate { name, .. } = expr {
441        let pred_name = interner.resolve(*name).to_string();
442        pred_name.contains("Accessible") || pred_name.contains("Reachable") || pred_name.contains("Next_Temporal")
443    } else {
444        false
445    }
446}
447
448/// Check if an expression is specifically Next_Temporal (not Accessible or Reachable).
449fn is_next_temporal_predicate<'a>(expr: &'a LogicExpr<'a>, interner: &Interner) -> bool {
450    if let LogicExpr::Predicate { name, .. } = expr {
451        let pred_name = interner.resolve(*name).to_string();
452        pred_name.contains("Next_Temporal")
453    } else {
454        false
455    }
456}
457
458/// Helper to extract a string from a Term for signal naming.
459fn term_to_string_helper<'a>(term: &'a Term<'a>, interner: &Interner) -> String {
460    match term {
461        Term::Constant(sym) | Term::Variable(sym) => interner.resolve(*sym).to_string(),
462        Term::Function(sym, _) => interner.resolve(*sym).to_string(),
463        _ => "unknown".to_string(),
464    }
465}
466
467#[cfg(test)]
468mod block_header_robustness {
469    use super::*;
470
471    /// A property sentence followed by a `## Theorem`/`## Main` block (multi-section
472    /// editor content) must still synthesize the property, not fail with a parse error
473    /// on the trailing block header.
474    #[test]
475    fn property_followed_by_a_block_synthesizes() {
476        let spec = "Always, if request then eventually grant.\n## Theorem t:\n  It holds.";
477        let r = synthesize_sva_from_spec(spec, "clk");
478        assert!(r.is_ok(), "expected SVA, got error: {:?}", r.err());
479        assert!(r.unwrap().sva_text.contains("property"));
480    }
481
482    /// A property INSIDE a leading block (header first) already works via the parser's
483    /// leading-header handling — guard that the fix doesn't break it.
484    #[test]
485    fn property_inside_a_leading_block_still_works() {
486        let spec = "## Hardware\nAlways, if request then eventually grant.";
487        let r = synthesize_sva_from_spec(spec, "clk");
488        assert!(r.is_ok(), "expected SVA, got error: {:?}", r.err());
489    }
490}