Skip to main content

logicaffeine_compile/codegen_sva/
sva_model.rs

1//! SVA Semantic Model
2//!
3//! Provides an AST for a subset of SystemVerilog Assertions, a parser
4//! for that subset, and structural equivalence checking.
5//!
6//! This model enables the Z3 semantic equivalence pipeline:
7//! FOL (from LOGOS) ↔ SVA (from LLM) checked for structural match.
8
9/// SVA expression AST — models a useful subset of SystemVerilog Assertions.
10#[derive(Debug, Clone)]
11pub enum SvaExpr {
12    /// Signal reference: `req`, `ack`, `data_out`
13    Signal(String),
14    /// Integer constant with bit width: `8'hFF`
15    Const(u64, u32),
16    /// Rising edge: `$rose(sig)`
17    Rose(Box<SvaExpr>),
18    /// Falling edge: `$fell(sig)`
19    Fell(Box<SvaExpr>),
20    /// Past value: `$past(sig, n)`
21    Past(Box<SvaExpr>, u32),
22    /// Conjunction: `a && b`
23    And(Box<SvaExpr>, Box<SvaExpr>),
24    /// Disjunction: `a || b`
25    Or(Box<SvaExpr>, Box<SvaExpr>),
26    /// Negation: `!a`
27    Not(Box<SvaExpr>),
28    /// Equality: `a == b`
29    Eq(Box<SvaExpr>, Box<SvaExpr>),
30    /// SVA implication: `a |-> b` (overlapping) or `a |=> b` (non-overlapping)
31    Implication {
32        antecedent: Box<SvaExpr>,
33        consequent: Box<SvaExpr>,
34        overlapping: bool,
35    },
36    /// Delay: `##[min:max] body`
37    Delay {
38        body: Box<SvaExpr>,
39        min: u32,
40        max: Option<u32>,
41    },
42    /// Sequence repetition: `body[*N]` or `body[*min:max]`
43    Repetition {
44        body: Box<SvaExpr>,
45        min: u32,
46        max: Option<u32>, // None = unbounded ($)
47    },
48    /// Strong eventually: `s_eventually(body)`
49    SEventually(Box<SvaExpr>),
50    /// Strong always: `s_always(body)`
51    SAlways(Box<SvaExpr>),
52    /// Stable: `$stable(sig)` — signal unchanged from previous cycle
53    Stable(Box<SvaExpr>),
54    /// Changed: `$changed(sig)` — signal changed from previous cycle
55    Changed(Box<SvaExpr>),
56    /// Disable condition: `disable iff (cond) body`
57    DisableIff {
58        condition: Box<SvaExpr>,
59        body: Box<SvaExpr>,
60    },
61    /// Next time: `nexttime(body)` or `nexttime[N](body)`
62    Nexttime(Box<SvaExpr>, u32),
63    /// Conditional property: `if (cond) P else Q`
64    IfElse {
65        condition: Box<SvaExpr>,
66        then_expr: Box<SvaExpr>,
67        else_expr: Box<SvaExpr>,
68    },
69    // ── IEEE 1800 Extended Constructs (Sprint 1B) ──
70    /// Inequality: `a != b`
71    NotEq(Box<SvaExpr>, Box<SvaExpr>),
72    /// Less than: `a < b`
73    LessThan(Box<SvaExpr>, Box<SvaExpr>),
74    /// Greater than: `a > b`
75    GreaterThan(Box<SvaExpr>, Box<SvaExpr>),
76    /// Less or equal: `a <= b`
77    LessEqual(Box<SvaExpr>, Box<SvaExpr>),
78    /// Greater or equal: `a >= b`
79    GreaterEqual(Box<SvaExpr>, Box<SvaExpr>),
80    /// Ternary: `cond ? a : b`
81    Ternary {
82        condition: Box<SvaExpr>,
83        then_expr: Box<SvaExpr>,
84        else_expr: Box<SvaExpr>,
85    },
86    /// Throughout: `sig throughout seq` — signal holds during entire sequence
87    Throughout {
88        signal: Box<SvaExpr>,
89        sequence: Box<SvaExpr>,
90    },
91    /// Within: `seq1 within seq2` — first sequence completes within second
92    Within {
93        inner: Box<SvaExpr>,
94        outer: Box<SvaExpr>,
95    },
96    /// First match: `first_match(seq)` — matches at first completion
97    FirstMatch(Box<SvaExpr>),
98    /// Intersect: `seq1 intersect seq2` — both sequences match with same length
99    Intersect {
100        left: Box<SvaExpr>,
101        right: Box<SvaExpr>,
102    },
103    // ── IEEE 1800 System Functions (Audit) ──
104    /// At most one bit set: `$onehot0(sig)` — popcount ≤ 1
105    OneHot0(Box<SvaExpr>),
106    /// Exactly one bit set: `$onehot(sig)` — popcount = 1
107    OneHot(Box<SvaExpr>),
108    /// Population count: `$countones(sig)` — returns integer
109    CountOnes(Box<SvaExpr>),
110    /// X/Z detection: `$isunknown(sig)` — always false in 2-state formal
111    IsUnknown(Box<SvaExpr>),
112    /// Sampled value: `$sampled(sig)` — identity in synchronous formal
113    Sampled(Box<SvaExpr>),
114    /// Bit width: `$bits(sig)` — returns integer
115    Bits(Box<SvaExpr>),
116    /// Ceiling log2: `$clog2(val)` — minimum bits to represent val
117    Clog2(Box<SvaExpr>),
118    // ── IEEE 1800 System Functions (Sprint 13) ──
119    /// Generalized bit counting: `$countbits(sig, '0', '1', ...)` (IEEE 20.9)
120    CountBits(Box<SvaExpr>, Vec<char>),
121    /// Parameter bound check: `$isunbounded(param)` (IEEE 20.9)
122    IsUnbounded(Box<SvaExpr>),
123    // ── Advanced Sequences (Audit) ──
124    /// Goto repetition: `sig[->N]` — N non-consecutive matches
125    GotoRepetition {
126        body: Box<SvaExpr>,
127        count: u32,
128    },
129    /// Non-consecutive repetition: `sig[=N]` or `sig[=min:max]`
130    NonConsecRepetition {
131        body: Box<SvaExpr>,
132        min: u32,
133        max: Option<u32>,
134    },
135    // ── Property Abort Operators (Audit) ──
136    /// Accept on: `accept_on(cond) body` — property passes if cond true
137    AcceptOn {
138        condition: Box<SvaExpr>,
139        body: Box<SvaExpr>,
140    },
141    /// Reject on: `reject_on(cond) body` — property fails if cond true
142    RejectOn {
143        condition: Box<SvaExpr>,
144        body: Box<SvaExpr>,
145    },
146    // ── Property Connectives (Sprint 1, IEEE 16.12.3-8) ──
147    /// Property negation: `not property_expr` (IEEE 16.12.3)
148    /// Distinct from boolean `!` — negates temporal property evaluation.
149    /// Flips strength: not(weak) → strong, not(strong) → weak (IEEE 16.12.15).
150    PropertyNot(Box<SvaExpr>),
151    /// Property implication: `p implies q` (IEEE 16.12.8)
152    /// Distinct from sequence `|->` — property-level `not p or q`.
153    PropertyImplies(Box<SvaExpr>, Box<SvaExpr>),
154    /// Property biconditional: `p iff q` (IEEE 16.12.8)
155    /// Equivalent to `(p implies q) and (q implies p)`.
156    PropertyIff(Box<SvaExpr>, Box<SvaExpr>),
157    // ── LTL Temporal Operators (Sprint 2, IEEE 16.12.11-13) ──
158    /// Weak unbounded always: `always p` (IEEE 16.12.11)
159    /// Passes if trace ends (weak semantics).
160    Always(Box<SvaExpr>),
161    /// Weak bounded always: `always [m:n] p` (IEEE 16.12.11)
162    /// max=None means $ (weak allows $).
163    AlwaysBounded { body: Box<SvaExpr>, min: u32, max: Option<u32> },
164    /// Strong bounded always: `s_always [m:n] p` (IEEE 16.12.11)
165    /// All ticks must exist. NO $ allowed.
166    SAlwaysBounded { body: Box<SvaExpr>, min: u32, max: u32 },
167    /// Weak bounded eventually: `eventually [m:n] p` (IEEE 16.12.13)
168    /// Range must be bounded (no $).
169    EventuallyBounded { body: Box<SvaExpr>, min: u32, max: u32 },
170    /// Strong bounded eventually: `s_eventually [m:n] p` (IEEE 16.12.13)
171    /// CAN use $ (max=None means $).
172    SEventuallyBounded { body: Box<SvaExpr>, min: u32, max: Option<u32> },
173    /// Until operator with 4 variants (IEEE 16.12.12):
174    /// `p until q` (weak, non-overlapping)
175    /// `p s_until q` (strong, non-overlapping)
176    /// `p until_with q` (weak, overlapping)
177    /// `p s_until_with q` (strong, overlapping)
178    Until { lhs: Box<SvaExpr>, rhs: Box<SvaExpr>, strong: bool, inclusive: bool },
179    // ── Strong/Weak, Advanced Temporal & Sync Abort (Sprint 3, IEEE 16.12.2, 16.12.9-10, 16.12.14, 16.12.16) ──
180    /// Strong sequence: `strong(seq)` (IEEE 16.12.2) — match must exist within bound
181    Strong(Box<SvaExpr>),
182    /// Weak sequence: `weak(seq)` (IEEE 16.12.2) — no match needed if bound exhausted
183    Weak(Box<SvaExpr>),
184    /// Strong nexttime: `s_nexttime(body)` or `s_nexttime[N](body)` (IEEE 16.12.10)
185    SNexttime(Box<SvaExpr>, u32),
186    /// Followed-by: `seq #-# prop` (overlapping) or `seq #=# prop` (non-overlapping) (IEEE 16.12.9)
187    FollowedBy { antecedent: Box<SvaExpr>, consequent: Box<SvaExpr>, overlapping: bool },
188    /// Property case: `case(expr) val: prop; ... default: prop; endcase` (IEEE 16.12.16)
189    PropertyCase { expression: Box<SvaExpr>, items: Vec<(Vec<SvaExpr>, Box<SvaExpr>)>, default: Option<Box<SvaExpr>> },
190    /// Synchronous accept abort: `sync_accept_on(cond) body` (IEEE 16.12.14)
191    SyncAcceptOn { condition: Box<SvaExpr>, body: Box<SvaExpr> },
192    /// Synchronous reject abort: `sync_reject_on(cond) body` (IEEE 16.12.14)
193    SyncRejectOn { condition: Box<SvaExpr>, body: Box<SvaExpr> },
194    // ── Sequence-Level AND & OR (Sprint 5, IEEE 16.9.5, 16.9.7) ──
195    /// Sequence AND: both operands match, composite ends at whichever finishes last
196    SequenceAnd(Box<SvaExpr>, Box<SvaExpr>),
197    /// Sequence OR: at least one matches, composite match set is union
198    SequenceOr(Box<SvaExpr>, Box<SvaExpr>),
199    // ── Assertion Directives (Sprint 7, IEEE 16.2-4, 16.14) ──
200    /// Immediate assertion: `assert(expr)`, `assert #0(expr)`, `assert final(expr)`
201    ImmediateAssert {
202        expression: Box<SvaExpr>,
203        deferred: Option<ImmediateDeferred>,
204    },
205    // ── Complex Data Types (Sprint 13, IEEE 16.6, 20.9) ──
206    /// Field access: `req.addr` or `req.header.id`
207    FieldAccess { signal: Box<SvaExpr>, field: String },
208    /// Enum literal: `ST_READ` or `state_t::READ`
209    EnumLiteral { type_name: Option<String>, value: String },
210    // ── Endpoint Methods (Sprint 14, IEEE 16.9.11) ──
211    /// Triggered endpoint: `seq_name.triggered`
212    Triggered(String),
213    /// Matched endpoint: `seq_name.matched`
214    Matched(String),
215    // ── Bitwise Operators (Sprint 15, IEEE 16.6) ──
216    BitAnd(Box<SvaExpr>, Box<SvaExpr>),
217    BitOr(Box<SvaExpr>, Box<SvaExpr>),
218    BitXor(Box<SvaExpr>, Box<SvaExpr>),
219    BitNot(Box<SvaExpr>),
220    ReductionAnd(Box<SvaExpr>),
221    ReductionOr(Box<SvaExpr>),
222    ReductionXor(Box<SvaExpr>),
223    BitSelect { signal: Box<SvaExpr>, index: Box<SvaExpr> },
224    PartSelect { signal: Box<SvaExpr>, high: u32, low: u32 },
225    Concat(Vec<SvaExpr>),
226    // ── Local Variables (Sprint 10, IEEE 16.10) ──
227    /// Sequence match with local variable assignments:
228    /// `(expr, v = rhs, w = rhs2)` — expression matches AND variables assigned
229    SequenceAction {
230        expression: Box<SvaExpr>,
231        assignments: Vec<(String, Box<SvaExpr>)>,
232    },
233    /// Local variable reference within a sequence/property
234    LocalVar(String),
235    // ── Let Construct (Sprint 16, IEEE 11.12) ──
236    /// const' cast: `const'(expr)` — freezes value at queue time
237    ConstCast(Box<SvaExpr>),
238    // ── Multi-Clock (Sprint 12, IEEE 16.13) ──
239    /// Clocked expression: `@(posedge clk) body` — preserves clock annotation for multi-clock
240    Clocked { clock: String, edge: ClockEdge, body: Box<SvaExpr> },
241    // ── IEEE 1800-2023 Additions (Sprint 23) ──
242    /// Array map method: `A.map(x) with (expr)` (IEEE 7.12, 2023)
243    ArrayMap { array: Box<SvaExpr>, iterator: String, with_expr: Box<SvaExpr> },
244    /// Type operator: `type(this)` (IEEE 6.23, 2023)
245    TypeThis,
246    /// Real literal constant: `1.5`, `1.2E3` (IEEE 5.7.2, 2023)
247    RealConst(f64),
248}
249
250/// Clock edge type
251#[derive(Debug, Clone, PartialEq)]
252pub enum ClockEdge {
253    Posedge,
254    Negedge,
255    Edge, // any edge
256}
257
258/// Deferred assertion timing (IEEE 16.4)
259#[derive(Debug, Clone, PartialEq)]
260pub enum ImmediateDeferred {
261    /// `#0` — observed region
262    Observed,
263    /// `final` — final simulation phase
264    Final,
265}
266
267/// Assertion directive kind (IEEE 16.14)
268#[derive(Debug, Clone, PartialEq)]
269pub enum SvaDirectiveKind {
270    Assert,
271    Assume,
272    Cover,
273    CoverSequence,
274    Restrict,
275}
276
277/// A concurrent assertion directive (IEEE 16.14)
278#[derive(Debug, Clone)]
279pub struct SvaDirective {
280    pub kind: SvaDirectiveKind,
281    pub property: SvaExpr,
282    pub label: Option<String>,
283    pub clock: Option<String>,
284    pub disable_iff: Option<SvaExpr>,
285    pub action_pass: Option<String>,
286    pub action_fail: Option<String>,
287}
288
289/// Port type for named sequence/property declarations (IEEE 16.8, 16.12)
290#[derive(Debug, Clone, PartialEq)]
291pub enum SvaPortType {
292    Untyped,
293    Bit,
294    Sequence,
295    Property,
296}
297
298/// A port in a named sequence or property declaration
299#[derive(Debug, Clone)]
300pub struct SvaPort {
301    pub name: String,
302    pub port_type: SvaPortType,
303    pub default: Option<SvaExpr>,
304}
305
306/// Named sequence declaration (IEEE 16.8)
307#[derive(Debug, Clone)]
308pub struct SequenceDecl {
309    pub name: String,
310    pub ports: Vec<SvaPort>,
311    pub body: SvaExpr,
312}
313
314/// Named property declaration (IEEE 16.12)
315#[derive(Debug, Clone)]
316pub struct PropertyDecl {
317    pub name: String,
318    pub ports: Vec<SvaPort>,
319    pub body: SvaExpr,
320}
321
322/// Let declaration (IEEE 11.12) — pure expression substitution
323#[derive(Debug, Clone)]
324pub struct LetDecl {
325    pub name: String,
326    pub ports: Vec<SvaPort>,
327    pub body: SvaExpr,
328}
329
330/// Dist weight kind (IEEE 18.5.4)
331#[derive(Debug, Clone, PartialEq)]
332pub enum DistKind {
333    /// `:=` — weight per value
334    PerValue,
335    /// `:/` — weight distributed across range
336    PerRange,
337}
338
339/// A dist item — value or range with weight
340#[derive(Debug, Clone)]
341pub struct DistItem {
342    pub min: u64,
343    pub max: Option<u64>,
344    pub weight: u64,
345    pub kind: DistKind,
346}
347
348/// Checker declaration (IEEE Chapter 17)
349#[derive(Debug, Clone)]
350pub struct CheckerDecl {
351    pub name: String,
352    pub ports: Vec<SvaPort>,
353    pub rand_vars: Vec<RandVar>,
354    pub assertions: Vec<SvaDirective>,
355}
356
357/// Type discriminant for random variables (IEEE 17.7, extended in 2023)
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum RandVarType {
360    /// Bitvector with width: `rand bit [N-1:0]`
361    BitVec(u32),
362    /// IEEE 754 double: `rand real` (IEEE 1800-2023)
363    Real,
364}
365
366/// Random variable in a checker (IEEE 17.7)
367#[derive(Debug, Clone)]
368pub struct RandVar {
369    pub name: String,
370    pub var_type: RandVarType,
371    pub is_const: bool,
372}
373
374/// Resolve a sequence instance by substituting actuals for formals.
375pub fn resolve_sequence_instance(
376    decls: &[SequenceDecl],
377    name: &str,
378    args: &[SvaExpr],
379) -> Result<SvaExpr, SvaParseError> {
380    let decl = decls.iter().find(|d| d.name == name).ok_or_else(|| SvaParseError {
381        message: format!("undeclared sequence: '{}'", name),
382    })?;
383
384    let expected = decl.ports.len();
385    let provided = args.len();
386    // Count ports without defaults to determine minimum args
387    let min_args = decl.ports.iter().filter(|p| p.default.is_none()).count();
388    if provided < min_args || provided > expected {
389        return Err(SvaParseError {
390            message: format!(
391                "sequence '{}' expects {}-{} arguments, got {}",
392                name, min_args, expected, provided
393            ),
394        });
395    }
396
397    let mut result = decl.body.clone();
398    for (i, port) in decl.ports.iter().enumerate() {
399        let actual = if i < args.len() {
400            args[i].clone()
401        } else if let Some(ref default) = port.default {
402            default.clone()
403        } else {
404            return Err(SvaParseError {
405                message: format!("missing argument for port '{}' in sequence '{}'", port.name, name),
406            });
407        };
408        result = substitute_signal(&result, &port.name, &actual);
409    }
410    Ok(result)
411}
412
413/// Substitute all occurrences of signal `name` with `replacement` in an expression.
414/// Handles all 78 SvaExpr variants recursively.
415fn substitute_signal(expr: &SvaExpr, name: &str, replacement: &SvaExpr) -> SvaExpr {
416    let sub = |e: &SvaExpr| Box::new(substitute_signal(e, name, replacement));
417    let sub_vec = |v: &[SvaExpr]| v.iter().map(|e| substitute_signal(e, name, replacement)).collect::<Vec<_>>();
418
419    match expr {
420        // ── Terminals ──
421        SvaExpr::Signal(s) if s == name => replacement.clone(),
422        SvaExpr::Signal(_) | SvaExpr::Const(_, _) => expr.clone(),
423        SvaExpr::LocalVar(s) if s == name => replacement.clone(),
424        SvaExpr::LocalVar(_) => expr.clone(),
425        SvaExpr::Triggered(s) => SvaExpr::Triggered(s.clone()),
426        SvaExpr::Matched(s) => SvaExpr::Matched(s.clone()),
427        SvaExpr::EnumLiteral { type_name, value } => SvaExpr::EnumLiteral {
428            type_name: type_name.clone(), value: value.clone(),
429        },
430
431        // ── Unary wrappers ──
432        SvaExpr::Rose(inner) => SvaExpr::Rose(sub(inner)),
433        SvaExpr::Fell(inner) => SvaExpr::Fell(sub(inner)),
434        SvaExpr::Not(inner) => SvaExpr::Not(sub(inner)),
435        SvaExpr::Stable(inner) => SvaExpr::Stable(sub(inner)),
436        SvaExpr::Changed(inner) => SvaExpr::Changed(sub(inner)),
437        SvaExpr::SEventually(inner) => SvaExpr::SEventually(sub(inner)),
438        SvaExpr::SAlways(inner) => SvaExpr::SAlways(sub(inner)),
439        SvaExpr::Always(inner) => SvaExpr::Always(sub(inner)),
440        SvaExpr::FirstMatch(inner) => SvaExpr::FirstMatch(sub(inner)),
441        SvaExpr::Strong(inner) => SvaExpr::Strong(sub(inner)),
442        SvaExpr::Weak(inner) => SvaExpr::Weak(sub(inner)),
443        SvaExpr::PropertyNot(inner) => SvaExpr::PropertyNot(sub(inner)),
444        SvaExpr::OneHot0(inner) => SvaExpr::OneHot0(sub(inner)),
445        SvaExpr::OneHot(inner) => SvaExpr::OneHot(sub(inner)),
446        SvaExpr::CountOnes(inner) => SvaExpr::CountOnes(sub(inner)),
447        SvaExpr::IsUnknown(inner) => SvaExpr::IsUnknown(sub(inner)),
448        SvaExpr::Sampled(inner) => SvaExpr::Sampled(sub(inner)),
449        SvaExpr::Bits(inner) => SvaExpr::Bits(sub(inner)),
450        SvaExpr::Clog2(inner) => SvaExpr::Clog2(sub(inner)),
451        SvaExpr::IsUnbounded(inner) => SvaExpr::IsUnbounded(sub(inner)),
452        SvaExpr::BitNot(inner) => SvaExpr::BitNot(sub(inner)),
453        SvaExpr::ReductionAnd(inner) => SvaExpr::ReductionAnd(sub(inner)),
454        SvaExpr::ReductionOr(inner) => SvaExpr::ReductionOr(sub(inner)),
455        SvaExpr::ReductionXor(inner) => SvaExpr::ReductionXor(sub(inner)),
456        SvaExpr::ConstCast(inner) => SvaExpr::ConstCast(sub(inner)),
457
458        // ── Unary with extra data ──
459        SvaExpr::Past(inner, n) => SvaExpr::Past(sub(inner), *n),
460        SvaExpr::Nexttime(inner, n) => SvaExpr::Nexttime(sub(inner), *n),
461        SvaExpr::SNexttime(inner, n) => SvaExpr::SNexttime(sub(inner), *n),
462        SvaExpr::CountBits(inner, chars) => SvaExpr::CountBits(sub(inner), chars.clone()),
463        SvaExpr::GotoRepetition { body, count } => SvaExpr::GotoRepetition {
464            body: sub(body), count: *count,
465        },
466
467        // ── Binary ──
468        SvaExpr::And(l, r) => SvaExpr::And(sub(l), sub(r)),
469        SvaExpr::Or(l, r) => SvaExpr::Or(sub(l), sub(r)),
470        SvaExpr::Eq(l, r) => SvaExpr::Eq(sub(l), sub(r)),
471        SvaExpr::NotEq(l, r) => SvaExpr::NotEq(sub(l), sub(r)),
472        SvaExpr::LessThan(l, r) => SvaExpr::LessThan(sub(l), sub(r)),
473        SvaExpr::GreaterThan(l, r) => SvaExpr::GreaterThan(sub(l), sub(r)),
474        SvaExpr::LessEqual(l, r) => SvaExpr::LessEqual(sub(l), sub(r)),
475        SvaExpr::GreaterEqual(l, r) => SvaExpr::GreaterEqual(sub(l), sub(r)),
476        SvaExpr::PropertyImplies(l, r) => SvaExpr::PropertyImplies(sub(l), sub(r)),
477        SvaExpr::PropertyIff(l, r) => SvaExpr::PropertyIff(sub(l), sub(r)),
478        SvaExpr::SequenceAnd(l, r) => SvaExpr::SequenceAnd(sub(l), sub(r)),
479        SvaExpr::SequenceOr(l, r) => SvaExpr::SequenceOr(sub(l), sub(r)),
480        SvaExpr::BitAnd(l, r) => SvaExpr::BitAnd(sub(l), sub(r)),
481        SvaExpr::BitOr(l, r) => SvaExpr::BitOr(sub(l), sub(r)),
482        SvaExpr::BitXor(l, r) => SvaExpr::BitXor(sub(l), sub(r)),
483
484        // ── Struct-like with named fields ──
485        SvaExpr::Implication { antecedent, consequent, overlapping } => SvaExpr::Implication {
486            antecedent: sub(antecedent), consequent: sub(consequent), overlapping: *overlapping,
487        },
488        SvaExpr::Delay { body, min, max } => SvaExpr::Delay {
489            body: sub(body), min: *min, max: *max,
490        },
491        SvaExpr::Repetition { body, min, max } => SvaExpr::Repetition {
492            body: sub(body), min: *min, max: *max,
493        },
494        SvaExpr::NonConsecRepetition { body, min, max } => SvaExpr::NonConsecRepetition {
495            body: sub(body), min: *min, max: *max,
496        },
497        SvaExpr::DisableIff { condition, body } => SvaExpr::DisableIff {
498            condition: sub(condition), body: sub(body),
499        },
500        SvaExpr::IfElse { condition, then_expr, else_expr } => SvaExpr::IfElse {
501            condition: sub(condition), then_expr: sub(then_expr), else_expr: sub(else_expr),
502        },
503        SvaExpr::Ternary { condition, then_expr, else_expr } => SvaExpr::Ternary {
504            condition: sub(condition), then_expr: sub(then_expr), else_expr: sub(else_expr),
505        },
506        SvaExpr::Throughout { signal, sequence } => SvaExpr::Throughout {
507            signal: sub(signal), sequence: sub(sequence),
508        },
509        SvaExpr::Within { inner, outer } => SvaExpr::Within {
510            inner: sub(inner), outer: sub(outer),
511        },
512        SvaExpr::Intersect { left, right } => SvaExpr::Intersect {
513            left: sub(left), right: sub(right),
514        },
515        SvaExpr::AcceptOn { condition, body } => SvaExpr::AcceptOn {
516            condition: sub(condition), body: sub(body),
517        },
518        SvaExpr::RejectOn { condition, body } => SvaExpr::RejectOn {
519            condition: sub(condition), body: sub(body),
520        },
521        SvaExpr::SyncAcceptOn { condition, body } => SvaExpr::SyncAcceptOn {
522            condition: sub(condition), body: sub(body),
523        },
524        SvaExpr::SyncRejectOn { condition, body } => SvaExpr::SyncRejectOn {
525            condition: sub(condition), body: sub(body),
526        },
527        SvaExpr::FollowedBy { antecedent, consequent, overlapping } => SvaExpr::FollowedBy {
528            antecedent: sub(antecedent), consequent: sub(consequent), overlapping: *overlapping,
529        },
530        SvaExpr::Until { lhs, rhs, strong, inclusive } => SvaExpr::Until {
531            lhs: sub(lhs), rhs: sub(rhs), strong: *strong, inclusive: *inclusive,
532        },
533        SvaExpr::AlwaysBounded { body, min, max } => SvaExpr::AlwaysBounded {
534            body: sub(body), min: *min, max: *max,
535        },
536        SvaExpr::SAlwaysBounded { body, min, max } => SvaExpr::SAlwaysBounded {
537            body: sub(body), min: *min, max: *max,
538        },
539        SvaExpr::EventuallyBounded { body, min, max } => SvaExpr::EventuallyBounded {
540            body: sub(body), min: *min, max: *max,
541        },
542        SvaExpr::SEventuallyBounded { body, min, max } => SvaExpr::SEventuallyBounded {
543            body: sub(body), min: *min, max: *max,
544        },
545        SvaExpr::FieldAccess { signal, field } => SvaExpr::FieldAccess {
546            signal: sub(signal), field: field.clone(),
547        },
548        SvaExpr::BitSelect { signal, index } => SvaExpr::BitSelect {
549            signal: sub(signal), index: sub(index),
550        },
551        SvaExpr::PartSelect { signal, high, low } => SvaExpr::PartSelect {
552            signal: sub(signal), high: *high, low: *low,
553        },
554        SvaExpr::ImmediateAssert { expression, deferred } => SvaExpr::ImmediateAssert {
555            expression: sub(expression), deferred: deferred.clone(),
556        },
557
558        // ── Vec children ──
559        SvaExpr::Concat(items) => SvaExpr::Concat(sub_vec(items)),
560
561        // ── Complex structures ──
562        SvaExpr::PropertyCase { expression, items, default } => SvaExpr::PropertyCase {
563            expression: sub(expression),
564            items: items.iter().map(|(vals, prop)| {
565                (sub_vec(vals), sub(prop))
566            }).collect(),
567            default: default.as_ref().map(|d| sub(d)),
568        },
569        SvaExpr::SequenceAction { expression, assignments } => SvaExpr::SequenceAction {
570            expression: sub(expression),
571            assignments: assignments.iter().map(|(var_name, rhs)| {
572                (var_name.clone(), sub(rhs))
573            }).collect(),
574        },
575        SvaExpr::Clocked { clock, edge, body } => SvaExpr::Clocked {
576            clock: clock.clone(),
577            edge: edge.clone(),
578            body: sub(body),
579        },
580        SvaExpr::ArrayMap { array, iterator, with_expr } => SvaExpr::ArrayMap {
581            array: sub(array),
582            iterator: iterator.clone(),
583            with_expr: sub(with_expr),
584        },
585        SvaExpr::TypeThis => SvaExpr::TypeThis,
586        SvaExpr::RealConst(v) => SvaExpr::RealConst(*v),
587    }
588}
589
590/// Parse error for SVA subset.
591#[derive(Debug)]
592pub struct SvaParseError {
593    pub message: String,
594}
595
596impl std::fmt::Display for SvaParseError {
597    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598        write!(f, "SVA parse error: {}", self.message)
599    }
600}
601
602/// Parse a concurrent assertion directive (IEEE 16.14).
603///
604/// Handles: `assert property(...)`, `assume property(...)`, `cover property(...)`,
605/// `cover sequence(...)`, `restrict property(...)`.
606/// Optionally prefixed by a label: `label: assert property(...)`.
607/// Optionally contains action blocks: `assert property(p) $info("ok"); else $error("fail");`
608pub fn parse_sva_directive(input: &str) -> Result<SvaDirective, SvaParseError> {
609    let input = input.trim().trim_end_matches(';');
610    let input = input.trim();
611
612    // Check for label prefix: `label: directive...`
613    let (label, rest) = if let Some(colon_pos) = input.find(':') {
614        let potential_label = input[..colon_pos].trim();
615        // Only treat as label if it's a simple identifier (no spaces, no keywords)
616        if !potential_label.is_empty()
617            && potential_label.chars().all(|c| c.is_alphanumeric() || c == '_')
618            && !potential_label.starts_with("assert")
619            && !potential_label.starts_with("assume")
620            && !potential_label.starts_with("cover")
621            && !potential_label.starts_with("restrict")
622        {
623            (Some(potential_label.to_string()), input[colon_pos + 1..].trim())
624        } else {
625            (None, input)
626        }
627    } else {
628        (None, input)
629    };
630
631    // Determine directive kind
632    let (kind, after_kind) = if rest.starts_with("assert property") {
633        (SvaDirectiveKind::Assert, rest["assert property".len()..].trim())
634    } else if rest.starts_with("assume property") {
635        (SvaDirectiveKind::Assume, rest["assume property".len()..].trim())
636    } else if rest.starts_with("cover sequence") {
637        (SvaDirectiveKind::CoverSequence, rest["cover sequence".len()..].trim())
638    } else if rest.starts_with("cover property") {
639        (SvaDirectiveKind::Cover, rest["cover property".len()..].trim())
640    } else if rest.starts_with("restrict property") {
641        (SvaDirectiveKind::Restrict, rest["restrict property".len()..].trim())
642    } else {
643        return Err(SvaParseError {
644            message: format!("expected assertion directive, got: '{}'", rest),
645        });
646    };
647
648    // Parse the property expression in parentheses
649    if !after_kind.starts_with('(') {
650        return Err(SvaParseError {
651            message: format!("expected '(' after directive keyword, got: '{}'", after_kind),
652        });
653    }
654    let close = find_balanced_close(after_kind, 0).ok_or_else(|| SvaParseError {
655        message: "unbalanced parentheses in directive".to_string(),
656    })?;
657    let prop_str = &after_kind[1..close];
658    let after_prop = after_kind[close + 1..].trim();
659
660    // Parse optional clock: @(posedge clk)
661    let (clock, prop_rest) = if prop_str.trim().starts_with("@(") {
662        if let Some(clock_close) = prop_str.find(')') {
663            let clock_str = prop_str[2..clock_close].trim().to_string();
664            (Some(clock_str), prop_str[clock_close + 1..].trim())
665        } else {
666            (None, prop_str.trim())
667        }
668    } else {
669        (None, prop_str.trim())
670    };
671
672    // Parse optional disable iff
673    let (disable_iff, prop_body) = if prop_rest.starts_with("disable iff") {
674        let after_disable = prop_rest["disable iff".len()..].trim();
675        if after_disable.starts_with('(') {
676            if let Some(di_close) = find_balanced_close(after_disable, 0) {
677                let di_str = &after_disable[1..di_close];
678                let body = after_disable[di_close + 1..].trim();
679                (Some(parse_sva(di_str)?), body)
680            } else {
681                (None, prop_rest)
682            }
683        } else {
684            (None, prop_rest)
685        }
686    } else {
687        (None, prop_rest)
688    };
689
690    let property = parse_sva(prop_body)?;
691
692    // Parse optional action blocks after the closing paren
693    let mut action_pass = None;
694    let mut action_fail = None;
695    let remaining = after_prop;
696    if !remaining.is_empty() {
697        // Look for `else` keyword to separate pass/fail, but skip occurrences
698        // inside string literals (both regular "..." and triple-quoted """...""").
699        if let Some(else_pos) = find_else_outside_strings(remaining) {
700            let pass_part = remaining[..else_pos].trim();
701            let fail_part = remaining[else_pos + 4..].trim();
702            if !pass_part.is_empty() {
703                action_pass = Some(pass_part.trim_end_matches(';').trim().to_string());
704            }
705            if !fail_part.is_empty() {
706                action_fail = Some(fail_part.trim_end_matches(';').trim().to_string());
707            }
708        } else if remaining.starts_with("$") || remaining.starts_with("else") {
709            let part = remaining.trim_end_matches(';').trim();
710            if remaining.starts_with("else") {
711                action_fail = Some(part["else".len()..].trim().to_string());
712            } else {
713                action_pass = Some(part.to_string());
714            }
715        }
716    }
717
718    // Restrict property cannot have action blocks (IEEE 16.14.4)
719    if kind == SvaDirectiveKind::Restrict && (action_pass.is_some() || action_fail.is_some()) {
720        return Err(SvaParseError {
721            message: "restrict property cannot have action blocks".to_string(),
722        });
723    }
724
725    Ok(SvaDirective {
726        kind,
727        property,
728        label,
729        clock,
730        disable_iff,
731        action_pass,
732        action_fail,
733    })
734}
735
736/// Parse a subset of SVA text into an SvaExpr.
737///
738/// Supports: signals, `$rose()`, `$fell()`, `s_eventually()`,
739/// `!()`, `&&`, `||`, `==`, `|->`, `|=>`.
740pub fn parse_sva(input: &str) -> Result<SvaExpr, SvaParseError> {
741    let input = input.trim();
742
743    // Parse clock sensitivity prefix: @(posedge clk), @(negedge clk), @(edge clk)
744    // IEEE 16.13: preserve clock annotation as Clocked variant for multi-clock support
745    if input.starts_with("@(") {
746        if let Some(pos) = input.find(')') {
747            let clock_spec = input[2..pos].trim();
748            let rest = input[pos + 1..].trim();
749
750            let (edge, clock_name) = if clock_spec.starts_with("posedge ") {
751                (ClockEdge::Posedge, clock_spec[8..].trim().to_string())
752            } else if clock_spec.starts_with("negedge ") {
753                (ClockEdge::Negedge, clock_spec[8..].trim().to_string())
754            } else if clock_spec.starts_with("edge ") {
755                (ClockEdge::Edge, clock_spec[5..].trim().to_string())
756            } else {
757                // Unknown edge type — treat as posedge
758                (ClockEdge::Posedge, clock_spec.to_string())
759            };
760
761            let body = parse_toplevel(rest)?;
762            return Ok(SvaExpr::Clocked {
763                clock: clock_name,
764                edge,
765                body: Box::new(body),
766            });
767        }
768    }
769
770    parse_toplevel(input)
771}
772
773fn parse_toplevel(input: &str) -> Result<SvaExpr, SvaParseError> {
774    let input = input.trim();
775
776    // Immediate assertions: assert(expr), assert #0(expr), assert final(expr) (Sprint 7)
777    if input.starts_with("assert ") || input.starts_with("assert(") || input.starts_with("assert#") {
778        let rest = input["assert".len()..].trim();
779        if rest.starts_with("#0") {
780            let body = rest[2..].trim();
781            if body.starts_with('(') {
782                if let Some(close) = find_balanced_close(body, 0) {
783                    let inner = &body[1..close];
784                    return Ok(SvaExpr::ImmediateAssert {
785                        expression: Box::new(parse_implication(inner)?),
786                        deferred: Some(ImmediateDeferred::Observed),
787                    });
788                }
789            }
790        } else if rest.starts_with("final") {
791            let body = rest[5..].trim();
792            if body.starts_with('(') {
793                if let Some(close) = find_balanced_close(body, 0) {
794                    let inner = &body[1..close];
795                    return Ok(SvaExpr::ImmediateAssert {
796                        expression: Box::new(parse_implication(inner)?),
797                        deferred: Some(ImmediateDeferred::Final),
798                    });
799                }
800            }
801        } else if rest.starts_with('(') {
802            if let Some(close) = find_balanced_close(rest, 0) {
803                let inner = &rest[1..close];
804                return Ok(SvaExpr::ImmediateAssert {
805                    expression: Box::new(parse_implication(inner)?),
806                    deferred: None,
807                });
808            }
809        }
810    }
811
812    // sync_accept_on(cond) body — synchronous property abort (Sprint 3)
813    if input.starts_with("sync_accept_on") {
814        let rest = input["sync_accept_on".len()..].trim();
815        if rest.starts_with('(') {
816            if let Some(close) = find_balanced_close(rest, 0) {
817                let cond = &rest[1..close];
818                let body = rest[close + 1..].trim();
819                return Ok(SvaExpr::SyncAcceptOn {
820                    condition: Box::new(parse_implication(cond)?),
821                    body: Box::new(parse_toplevel(body)?),
822                });
823            }
824        }
825    }
826
827    // sync_reject_on(cond) body — synchronous property abort (Sprint 3)
828    if input.starts_with("sync_reject_on") {
829        let rest = input["sync_reject_on".len()..].trim();
830        if rest.starts_with('(') {
831            if let Some(close) = find_balanced_close(rest, 0) {
832                let cond = &rest[1..close];
833                let body = rest[close + 1..].trim();
834                return Ok(SvaExpr::SyncRejectOn {
835                    condition: Box::new(parse_implication(cond)?),
836                    body: Box::new(parse_toplevel(body)?),
837                });
838            }
839        }
840    }
841
842    // accept_on(cond) body — property abort operators
843    if input.starts_with("accept_on") {
844        let rest = input["accept_on".len()..].trim();
845        if rest.starts_with('(') {
846            if let Some(close) = find_balanced_close(rest, 0) {
847                let cond = &rest[1..close];
848                let body = rest[close + 1..].trim();
849                return Ok(SvaExpr::AcceptOn {
850                    condition: Box::new(parse_implication(cond)?),
851                    body: Box::new(parse_toplevel(body)?),
852                });
853            }
854        }
855    }
856
857    // reject_on(cond) body
858    if input.starts_with("reject_on") {
859        let rest = input["reject_on".len()..].trim();
860        if rest.starts_with('(') {
861            if let Some(close) = find_balanced_close(rest, 0) {
862                let cond = &rest[1..close];
863                let body = rest[close + 1..].trim();
864                return Ok(SvaExpr::RejectOn {
865                    condition: Box::new(parse_implication(cond)?),
866                    body: Box::new(parse_toplevel(body)?),
867                });
868            }
869        }
870    }
871
872    // disable iff (cond) body — must be checked before implication
873    if input.starts_with("disable iff") {
874        let rest = input["disable iff".len()..].trim();
875        if rest.starts_with('(') {
876            if let Some(close) = find_balanced_close(rest, 0) {
877                let cond = &rest[1..close];
878                let body = rest[close + 1..].trim();
879                return Ok(SvaExpr::DisableIff {
880                    condition: Box::new(parse_implication(cond)?),
881                    body: Box::new(parse_implication(body)?),
882                });
883            }
884        }
885    }
886
887    // if (cond) P else Q — must be checked before implication
888    if input.starts_with("if ") || input.starts_with("if(") {
889        let rest = input[2..].trim();
890        if rest.starts_with('(') {
891            if let Some(close) = find_balanced_close(rest, 0) {
892                let cond = &rest[1..close];
893                let after_cond = rest[close + 1..].trim();
894                if let Some(else_pos) = find_else_keyword(after_cond) {
895                    let then_part = after_cond[..else_pos].trim();
896                    let else_part = after_cond[else_pos + 4..].trim();
897                    return Ok(SvaExpr::IfElse {
898                        condition: Box::new(parse_implication(cond)?),
899                        then_expr: Box::new(parse_implication(then_part)?),
900                        else_expr: Box::new(parse_implication(else_part)?),
901                    });
902                } else {
903                    return Ok(SvaExpr::IfElse {
904                        condition: Box::new(parse_implication(cond)?),
905                        then_expr: Box::new(parse_implication(after_cond)?),
906                        else_expr: Box::new(SvaExpr::Signal("1".to_string())),
907                    });
908                }
909            }
910        }
911    }
912
913    parse_implication(input)
914}
915
916fn parse_implication(input: &str) -> Result<SvaExpr, SvaParseError> {
917    // Check for |-> or |=> or #-# or #=#
918    // Scan not inside parentheses
919    let mut depth = 0i32;
920    let chars: Vec<char> = input.chars().collect();
921    for i in 0..chars.len().saturating_sub(2) {
922        match chars[i] {
923            '(' => depth += 1,
924            ')' => depth -= 1,
925            '|' if depth == 0 => {
926                if i + 2 < chars.len() && chars[i + 1] == '-' && chars[i + 2] == '>' {
927                    let lhs = input[..i].trim();
928                    let rhs = input[i + 3..].trim();
929                    return Ok(SvaExpr::Implication {
930                        antecedent: Box::new(parse_property_implies(lhs)?),
931                        consequent: Box::new(parse_property_implies(rhs)?),
932                        overlapping: true,
933                    });
934                }
935                if i + 2 < chars.len() && chars[i + 1] == '=' && chars[i + 2] == '>' {
936                    let lhs = input[..i].trim();
937                    let rhs = input[i + 3..].trim();
938                    return Ok(SvaExpr::Implication {
939                        antecedent: Box::new(parse_property_implies(lhs)?),
940                        consequent: Box::new(parse_property_implies(rhs)?),
941                        overlapping: false,
942                    });
943                }
944            }
945            // #-# (followed-by overlapping) and #=# (followed-by non-overlapping)
946            '#' if depth == 0 && i + 2 < chars.len() => {
947                if chars[i + 1] == '-' && chars[i + 2] == '#' {
948                    let lhs = input[..i].trim();
949                    let rhs = input[i + 3..].trim();
950                    if !lhs.is_empty() {
951                        return Ok(SvaExpr::FollowedBy {
952                            antecedent: Box::new(parse_property_implies(lhs)?),
953                            consequent: Box::new(parse_property_implies(rhs)?),
954                            overlapping: true,
955                        });
956                    }
957                }
958                if chars[i + 1] == '=' && chars[i + 2] == '#' {
959                    let lhs = input[..i].trim();
960                    let rhs = input[i + 3..].trim();
961                    if !lhs.is_empty() {
962                        return Ok(SvaExpr::FollowedBy {
963                            antecedent: Box::new(parse_property_implies(lhs)?),
964                            consequent: Box::new(parse_property_implies(rhs)?),
965                            overlapping: false,
966                        });
967                    }
968                }
969            }
970            _ => {}
971        }
972    }
973    parse_property_implies(input)
974}
975
976/// Parse property-level `implies` (IEEE 16.12.8).
977/// Lower precedence than `iff`, higher than `|->` / `|=>`.
978fn parse_property_implies(input: &str) -> Result<SvaExpr, SvaParseError> {
979    let input = input.trim();
980    if let Some(pos) = find_keyword_at_depth_0(input, "implies") {
981        let lhs = input[..pos].trim();
982        let rhs = input[pos + 7..].trim();
983        return Ok(SvaExpr::PropertyImplies(
984            Box::new(parse_property_iff(lhs)?),
985            Box::new(parse_property_implies(rhs)?), // right-associative
986        ));
987    }
988    parse_property_iff(input)
989}
990
991/// Parse property-level `iff` (IEEE 16.12.8).
992/// Higher precedence than `implies`, lower than `until`.
993fn parse_property_iff(input: &str) -> Result<SvaExpr, SvaParseError> {
994    let input = input.trim();
995    if let Some(pos) = find_keyword_at_depth_0(input, "iff") {
996        let lhs = input[..pos].trim();
997        let rhs = input[pos + 3..].trim();
998        return Ok(SvaExpr::PropertyIff(
999            Box::new(parse_until(lhs)?),
1000            Box::new(parse_property_iff(rhs)?), // right-associative
1001        ));
1002    }
1003    parse_until(input)
1004}
1005
1006/// Parse `until` / `s_until` / `until_with` / `s_until_with` (IEEE 16.12.12).
1007/// Higher precedence than `iff`, lower than `||`.
1008fn parse_until(input: &str) -> Result<SvaExpr, SvaParseError> {
1009    let input = input.trim();
1010    // Check in order: longest keywords first to avoid partial matches
1011    for (keyword, strong, inclusive) in &[
1012        ("s_until_with", true, true),
1013        ("until_with", false, true),
1014        ("s_until", true, false),
1015        ("until", false, false),
1016    ] {
1017        if let Some(pos) = find_keyword_at_depth_0(input, keyword) {
1018            let lhs = input[..pos].trim();
1019            let rhs = input[pos + keyword.len()..].trim();
1020            return Ok(SvaExpr::Until {
1021                lhs: Box::new(parse_or(lhs)?),
1022                rhs: Box::new(parse_until(rhs)?), // right-associative
1023                strong: *strong,
1024                inclusive: *inclusive,
1025            });
1026        }
1027    }
1028    parse_or(input)
1029}
1030
1031fn parse_or(input: &str) -> Result<SvaExpr, SvaParseError> {
1032    let mut depth = 0i32;
1033    let chars: Vec<char> = input.chars().collect();
1034    for i in 0..chars.len().saturating_sub(1) {
1035        match chars[i] {
1036            '(' => depth += 1,
1037            ')' => depth -= 1,
1038            '|' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '|' => {
1039                let lhs = input[..i].trim();
1040                let rhs = input[i + 2..].trim();
1041                return Ok(SvaExpr::Or(
1042                    Box::new(parse_seq_ops(lhs)?),
1043                    Box::new(parse_or(rhs)?),
1044                ));
1045            }
1046            _ => {}
1047        }
1048    }
1049    parse_seq_ops(input)
1050}
1051
1052/// Parse sequence-level operators: `and`, `or`, `throughout`, `within`, `intersect`.
1053/// `and` / `or` are sequence-level (thread/union semantics), distinct from `&&` / `||`.
1054/// These bind tighter than `||` but looser than `&&`.
1055fn parse_seq_ops(input: &str) -> Result<SvaExpr, SvaParseError> {
1056    let input_trimmed = input.trim();
1057
1058    // Check for keyword operators at depth 0
1059    // Must scan for these as whole words (not inside identifiers)
1060    // Check `throughout`, `within`, `intersect` first (higher precedence than `and`/`or`)
1061    for keyword in &["throughout", "within", "intersect"] {
1062        if let Some(pos) = find_keyword_at_depth_0(input_trimmed, keyword) {
1063            let lhs = input_trimmed[..pos].trim();
1064            let rhs = input_trimmed[pos + keyword.len()..].trim();
1065            return match *keyword {
1066                "throughout" => Ok(SvaExpr::Throughout {
1067                    signal: Box::new(parse_and(lhs)?),
1068                    sequence: Box::new(parse_and(rhs)?),
1069                }),
1070                "within" => Ok(SvaExpr::Within {
1071                    inner: Box::new(parse_and(lhs)?),
1072                    outer: Box::new(parse_and(rhs)?),
1073                }),
1074                "intersect" => Ok(SvaExpr::Intersect {
1075                    left: Box::new(parse_and(lhs)?),
1076                    right: Box::new(parse_and(rhs)?),
1077                }),
1078                _ => unreachable!(),
1079            };
1080        }
1081    }
1082
1083    // Sequence-level `or` (IEEE 16.9.7) — union semantics
1084    // Check before `and` since `or` has lower precedence
1085    if let Some(pos) = find_keyword_at_depth_0(input_trimmed, "or") {
1086        let lhs = input_trimmed[..pos].trim();
1087        let rhs = input_trimmed[pos + 2..].trim();
1088        return Ok(SvaExpr::SequenceOr(
1089            Box::new(parse_seq_and(lhs)?),
1090            Box::new(parse_seq_ops(rhs)?), // right-associative
1091        ));
1092    }
1093
1094    parse_seq_and(input)
1095}
1096
1097/// Parse sequence-level `and` (IEEE 16.9.5) — thread semantics.
1098/// Higher precedence than sequence `or`, lower than `&&`.
1099fn parse_seq_and(input: &str) -> Result<SvaExpr, SvaParseError> {
1100    let input_trimmed = input.trim();
1101    if let Some(pos) = find_keyword_at_depth_0(input_trimmed, "and") {
1102        let lhs = input_trimmed[..pos].trim();
1103        let rhs = input_trimmed[pos + 3..].trim();
1104        return Ok(SvaExpr::SequenceAnd(
1105            Box::new(parse_and(lhs)?),
1106            Box::new(parse_seq_and(rhs)?), // right-associative
1107        ));
1108    }
1109    parse_and(input)
1110}
1111
1112/// Find a keyword at parenthesis depth 0, respecting word boundaries.
1113fn find_keyword_at_depth_0(input: &str, keyword: &str) -> Option<usize> {
1114    let mut depth = 0i32;
1115    let bytes = input.as_bytes();
1116    let klen = keyword.len();
1117    for i in 0..input.len() {
1118        match bytes[i] {
1119            b'(' => depth += 1,
1120            b')' => depth -= 1,
1121            _ if depth == 0 && i + klen <= input.len() => {
1122                if &input[i..i + klen] == keyword {
1123                    // Check word boundaries
1124                    let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
1125                    let after_ok = i + klen >= input.len() || !bytes[i + klen].is_ascii_alphanumeric();
1126                    if before_ok && after_ok {
1127                        return Some(i);
1128                    }
1129                }
1130            }
1131            _ => {}
1132        }
1133    }
1134    None
1135}
1136
1137fn parse_and(input: &str) -> Result<SvaExpr, SvaParseError> {
1138    let mut depth = 0i32;
1139    let chars: Vec<char> = input.chars().collect();
1140    for i in 0..chars.len().saturating_sub(1) {
1141        match chars[i] {
1142            '(' => depth += 1,
1143            ')' => depth -= 1,
1144            '&' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '&' => {
1145                let lhs = input[..i].trim();
1146                let rhs = input[i + 2..].trim();
1147                return Ok(SvaExpr::And(
1148                    Box::new(parse_sequence(lhs)?),
1149                    Box::new(parse_and(rhs)?),
1150                ));
1151            }
1152            _ => {}
1153        }
1154    }
1155    parse_sequence(input)
1156}
1157
1158/// Parse infix sequence concatenation: `req ##N ack` or `req ##[min:max] ack`.
1159/// In IEEE 1800, `##` between two expressions is a sequence delay operator.
1160/// This binds tighter than `&&` but looser than `==`/`!=`.
1161fn parse_sequence(input: &str) -> Result<SvaExpr, SvaParseError> {
1162    let input = input.trim();
1163    let bytes = input.as_bytes();
1164    let mut depth = 0i32;
1165
1166    // Scan for infix `##` at depth 0 (not at position 0 — that's prefix delay)
1167    // Start from i=0 to track parens, but only match ## when i > 0
1168    for i in 0..input.len().saturating_sub(1) {
1169        match bytes[i] {
1170            b'(' => { depth += 1; continue; }
1171            b')' => { depth -= 1; continue; }
1172            b'#' if depth == 0 && i > 0 && i + 1 < input.len() && bytes[i + 1] == b'#' => {
1173                // Found `##` not at start — this is infix sequence concatenation
1174                let lhs = input[..i].trim();
1175                if lhs.is_empty() { continue; }
1176                let delay_and_rhs = &input[i..]; // starts with "##..."
1177                // Parse the delay part: ##N or ##[min:max]
1178                let rest = &delay_and_rhs[2..];
1179                if rest.starts_with('[') {
1180                    // ##[min:max] rhs
1181                    if let Some(bracket_end) = rest.find(']') {
1182                        let range_str = &rest[1..bracket_end];
1183                        let rhs = rest[bracket_end + 1..].trim();
1184                        let parts: Vec<&str> = range_str.split(':').collect();
1185                        if parts.len() == 2 {
1186                            let min = parts[0].trim().parse::<u32>().unwrap_or(0);
1187                            let max_str = parts[1].trim();
1188                            let max = if max_str == "$" {
1189                                None // $ = unbounded
1190                            } else {
1191                                Some(max_str.parse::<u32>().unwrap_or(0))
1192                            };
1193                            return Ok(SvaExpr::Implication {
1194                                antecedent: Box::new(parse_eq(lhs)?),
1195                                consequent: Box::new(SvaExpr::Delay {
1196                                    body: Box::new(parse_sequence(rhs)?),
1197                                    min,
1198                                    max,
1199                                }),
1200                                overlapping: true,
1201                            });
1202                        }
1203                    }
1204                } else {
1205                    // ##N rhs — exact delay
1206                    let mut num_end = 0;
1207                    for c in rest.chars() {
1208                        if c.is_ascii_digit() { num_end += 1; } else { break; }
1209                    }
1210                    if num_end > 0 {
1211                        let n = rest[..num_end].parse::<u32>().unwrap_or(0);
1212                        let rhs = rest[num_end..].trim();
1213                        return Ok(SvaExpr::Implication {
1214                            antecedent: Box::new(parse_eq(lhs)?),
1215                            consequent: Box::new(SvaExpr::Delay {
1216                                body: Box::new(parse_sequence(rhs)?),
1217                                min: n,
1218                                max: Some(n), // exact: min == max
1219                            }),
1220                            overlapping: true,
1221                        });
1222                    }
1223                }
1224            }
1225            _ => {}
1226        }
1227    }
1228    parse_eq(input)
1229}
1230
1231fn parse_eq(input: &str) -> Result<SvaExpr, SvaParseError> {
1232    let mut depth = 0i32;
1233    let chars: Vec<char> = input.chars().collect();
1234    let len = chars.len();
1235
1236    // Scan for ternary: `cond ? then : else` (lowest precedence in this group)
1237    for i in 0..len {
1238        match chars[i] {
1239            '(' => depth += 1,
1240            ')' => depth -= 1,
1241            '?' if depth == 0 => {
1242                let cond = input[..i].trim();
1243                let rest = &input[i + 1..];
1244                // Find the matching ':'
1245                let mut d2 = 0i32;
1246                for j in 0..rest.len() {
1247                    match rest.as_bytes()[j] {
1248                        b'(' => d2 += 1,
1249                        b')' => d2 -= 1,
1250                        b':' if d2 == 0 => {
1251                            let then_part = rest[..j].trim();
1252                            let else_part = rest[j + 1..].trim();
1253                            return Ok(SvaExpr::Ternary {
1254                                condition: Box::new(parse_eq(cond)?),
1255                                then_expr: Box::new(parse_eq(then_part)?),
1256                                else_expr: Box::new(parse_eq(else_part)?),
1257                            });
1258                        }
1259                        _ => {}
1260                    }
1261                }
1262            }
1263            _ => {}
1264        }
1265    }
1266
1267    depth = 0;
1268    // Scan for comparison operators: ==, !=, <=, >=, <, >
1269    // Must check two-char operators before single-char ones
1270    // Track both () and [] depth so [-> and [= don't conflict with < and >
1271    for i in 0..len {
1272        match chars[i] {
1273            '(' | '[' => depth += 1,
1274            ')' | ']' => depth -= 1,
1275            _ if depth != 0 => {}
1276            '!' if i + 1 < len && chars[i + 1] == '=' => {
1277                let lhs = input[..i].trim();
1278                let rhs = input[i + 2..].trim();
1279                return Ok(SvaExpr::NotEq(
1280                    Box::new(parse_unary(lhs)?),
1281                    Box::new(parse_unary(rhs)?),
1282                ));
1283            }
1284            '=' if i + 1 < len && chars[i + 1] == '=' => {
1285                let lhs = input[..i].trim();
1286                let rhs = input[i + 2..].trim();
1287                return Ok(SvaExpr::Eq(
1288                    Box::new(parse_unary(lhs)?),
1289                    Box::new(parse_unary(rhs)?),
1290                ));
1291            }
1292            '<' if i + 1 < len && chars[i + 1] == '=' => {
1293                let lhs = input[..i].trim();
1294                let rhs = input[i + 2..].trim();
1295                return Ok(SvaExpr::LessEqual(
1296                    Box::new(parse_unary(lhs)?),
1297                    Box::new(parse_unary(rhs)?),
1298                ));
1299            }
1300            '>' if i + 1 < len && chars[i + 1] == '=' => {
1301                let lhs = input[..i].trim();
1302                let rhs = input[i + 2..].trim();
1303                return Ok(SvaExpr::GreaterEqual(
1304                    Box::new(parse_unary(lhs)?),
1305                    Box::new(parse_unary(rhs)?),
1306                ));
1307            }
1308            '<' if depth == 0 => {
1309                let lhs = input[..i].trim();
1310                let rhs = input[i + 1..].trim();
1311                return Ok(SvaExpr::LessThan(
1312                    Box::new(parse_unary(lhs)?),
1313                    Box::new(parse_unary(rhs)?),
1314                ));
1315            }
1316            '>' if depth == 0 => {
1317                let lhs = input[..i].trim();
1318                let rhs = input[i + 1..].trim();
1319                return Ok(SvaExpr::GreaterThan(
1320                    Box::new(parse_unary(lhs)?),
1321                    Box::new(parse_unary(rhs)?),
1322                ));
1323            }
1324            _ => {}
1325        }
1326    }
1327    parse_unary(input)
1328}
1329
1330fn parse_unary(input: &str) -> Result<SvaExpr, SvaParseError> {
1331    let input = input.trim();
1332
1333    // strong(seq) — sequence must complete within bound (Sprint 3)
1334    if let Some(result) = try_parse_function_call(input, "strong", |inner| {
1335        Ok(SvaExpr::Strong(Box::new(parse_implication(inner)?)))
1336    })? { return Ok(result); }
1337
1338    // weak(seq) — sequence may not complete (Sprint 3)
1339    if let Some(result) = try_parse_function_call(input, "weak", |inner| {
1340        Ok(SvaExpr::Weak(Box::new(parse_implication(inner)?)))
1341    })? { return Ok(result); }
1342
1343    // s_nexttime[N](body) — with explicit count (Sprint 3)
1344    if input.starts_with("s_nexttime[") {
1345        if let Some(bracket_end) = input.find(']') {
1346            let n_str = &input[11..bracket_end];
1347            if let Ok(n) = n_str.parse::<u32>() {
1348                let rest = input[bracket_end + 1..].trim();
1349                if rest.starts_with('(') {
1350                    if let Some(close) = find_balanced_close(rest, 0) {
1351                        let inner = &rest[1..close];
1352                        return Ok(SvaExpr::SNexttime(
1353                            Box::new(parse_implication(inner.trim())?),
1354                            n,
1355                        ));
1356                    }
1357                }
1358            }
1359        }
1360    }
1361
1362    // s_nexttime(body) — default count = 1 (Sprint 3)
1363    if let Some(result) = try_parse_function_call(input, "s_nexttime", |inner| {
1364        Ok(SvaExpr::SNexttime(Box::new(parse_implication(inner)?), 1))
1365    })? { return Ok(result); }
1366
1367    // Property negation: `not expr` (IEEE 16.12.3)
1368    // Must be before `!` check. `not` is a keyword (word boundary checked).
1369    if input.starts_with("not ") || input.starts_with("not(") {
1370        let rest = input[3..].trim();
1371        return Ok(SvaExpr::PropertyNot(Box::new(parse_unary(rest)?)));
1372    }
1373
1374    // always [m:n] body — bounded weak always (IEEE 16.12.11)
1375    // Must check before `always ` (unbounded) to avoid consuming the bracket
1376    if input.starts_with("always [") || input.starts_with("always[") {
1377        let rest = input["always".len()..].trim();
1378        if rest.starts_with('[') {
1379            if let Some(bracket_end) = rest.find(']') {
1380                let range_str = &rest[1..bracket_end];
1381                let body_str = rest[bracket_end + 1..].trim();
1382                let parts: Vec<&str> = range_str.split(':').collect();
1383                if parts.len() == 2 {
1384                    let min = parts[0].trim().parse::<u32>().map_err(|_| SvaParseError {
1385                        message: format!("invalid always min: '{}'", parts[0]),
1386                    })?;
1387                    let max_str = parts[1].trim();
1388                    let max = if max_str == "$" {
1389                        None // weak allows $
1390                    } else {
1391                        Some(max_str.parse::<u32>().map_err(|_| SvaParseError {
1392                            message: format!("invalid always max: '{}'", max_str),
1393                        })?)
1394                    };
1395                    return Ok(SvaExpr::AlwaysBounded {
1396                        body: Box::new(parse_unary(body_str)?),
1397                        min,
1398                        max,
1399                    });
1400                }
1401            }
1402        }
1403    }
1404
1405    // always body — unbounded weak always (IEEE 16.12.11)
1406    if input.starts_with("always ") || input.starts_with("always(") {
1407        let rest = input["always".len()..].trim();
1408        return Ok(SvaExpr::Always(Box::new(parse_unary(rest)?)));
1409    }
1410
1411    // s_always [m:n] body — bounded strong always (IEEE 16.12.11)
1412    // Must check BEFORE the existing s_always() function-call parse
1413    if input.starts_with("s_always [") || input.starts_with("s_always[") {
1414        let rest = input["s_always".len()..].trim();
1415        if rest.starts_with('[') {
1416            if let Some(bracket_end) = rest.find(']') {
1417                let range_str = &rest[1..bracket_end];
1418                let body_str = rest[bracket_end + 1..].trim();
1419                let parts: Vec<&str> = range_str.split(':').collect();
1420                if parts.len() == 2 {
1421                    let min = parts[0].trim().parse::<u32>().map_err(|_| SvaParseError {
1422                        message: format!("invalid s_always min: '{}'", parts[0]),
1423                    })?;
1424                    let max_str = parts[1].trim();
1425                    if max_str == "$" {
1426                        return Err(SvaParseError {
1427                            message: "s_always range must be bounded ($ not allowed)".to_string(),
1428                        });
1429                    }
1430                    let max = max_str.parse::<u32>().map_err(|_| SvaParseError {
1431                        message: format!("invalid s_always max: '{}'", max_str),
1432                    })?;
1433                    return Ok(SvaExpr::SAlwaysBounded {
1434                        body: Box::new(parse_unary(body_str)?),
1435                        min,
1436                        max,
1437                    });
1438                }
1439            }
1440        }
1441    }
1442
1443    // eventually [m:n] body — bounded weak eventually (IEEE 16.12.13)
1444    if input.starts_with("eventually [") || input.starts_with("eventually[") {
1445        let rest = input["eventually".len()..].trim();
1446        if rest.starts_with('[') {
1447            if let Some(bracket_end) = rest.find(']') {
1448                let range_str = &rest[1..bracket_end];
1449                let body_str = rest[bracket_end + 1..].trim();
1450                let parts: Vec<&str> = range_str.split(':').collect();
1451                if parts.len() == 2 {
1452                    let min = parts[0].trim().parse::<u32>().map_err(|_| SvaParseError {
1453                        message: format!("invalid eventually min: '{}'", parts[0]),
1454                    })?;
1455                    let max_str = parts[1].trim();
1456                    if max_str == "$" {
1457                        return Err(SvaParseError {
1458                            message: "weak eventually range must be bounded ($ not allowed)".to_string(),
1459                        });
1460                    }
1461                    let max = max_str.parse::<u32>().map_err(|_| SvaParseError {
1462                        message: format!("invalid eventually max: '{}'", max_str),
1463                    })?;
1464                    return Ok(SvaExpr::EventuallyBounded {
1465                        body: Box::new(parse_unary(body_str)?),
1466                        min,
1467                        max,
1468                    });
1469                }
1470            }
1471        }
1472    }
1473
1474    // s_eventually [m:n] body — bounded strong eventually (IEEE 16.12.13)
1475    // Must check BEFORE the existing s_eventually() function-call parse
1476    if input.starts_with("s_eventually [") || input.starts_with("s_eventually[") {
1477        let rest = input["s_eventually".len()..].trim();
1478        if rest.starts_with('[') {
1479            if let Some(bracket_end) = rest.find(']') {
1480                let range_str = &rest[1..bracket_end];
1481                let body_str = rest[bracket_end + 1..].trim();
1482                let parts: Vec<&str> = range_str.split(':').collect();
1483                if parts.len() == 2 {
1484                    let min = parts[0].trim().parse::<u32>().map_err(|_| SvaParseError {
1485                        message: format!("invalid s_eventually min: '{}'", parts[0]),
1486                    })?;
1487                    let max_str = parts[1].trim();
1488                    let max = if max_str == "$" {
1489                        None // strong eventually CAN use $
1490                    } else {
1491                        Some(max_str.parse::<u32>().map_err(|_| SvaParseError {
1492                            message: format!("invalid s_eventually max: '{}'", max_str),
1493                        })?)
1494                    };
1495                    return Ok(SvaExpr::SEventuallyBounded {
1496                        body: Box::new(parse_unary(body_str)?),
1497                        min,
1498                        max,
1499                    });
1500                }
1501            }
1502        }
1503    }
1504
1505    // Delay: ##N body or ##[min:max] body or ##[*] or ##[+]
1506    if input.starts_with("##") {
1507        let rest = &input[2..];
1508        if rest.starts_with('[') {
1509            // Check for ##[*] and ##[+] shorthands first
1510            // Convention: max: None = unbounded ($), max: Some(n) = bounded
1511            if rest.starts_with("[*]") {
1512                let body_str = rest[3..].trim();
1513                return Ok(SvaExpr::Delay {
1514                    body: Box::new(parse_unary(body_str)?),
1515                    min: 0,
1516                    max: None, // [*] = [0:$]
1517                });
1518            }
1519            if rest.starts_with("[+]") {
1520                let body_str = rest[3..].trim();
1521                return Ok(SvaExpr::Delay {
1522                    body: Box::new(parse_unary(body_str)?),
1523                    min: 1,
1524                    max: None, // [+] = [1:$]
1525                });
1526            }
1527            // ##[min:max] body
1528            if let Some(bracket_end) = rest.find(']') {
1529                let range_str = &rest[1..bracket_end];
1530                let body_str = rest[bracket_end + 1..].trim();
1531                let parts: Vec<&str> = range_str.split(':').collect();
1532                if parts.len() == 2 {
1533                    let min = parts[0].trim().parse::<u32>().map_err(|_| SvaParseError {
1534                        message: format!("invalid delay min: '{}'", parts[0]),
1535                    })?;
1536                    let max_str = parts[1].trim();
1537                    let max = if max_str == "$" {
1538                        None // $ = unbounded
1539                    } else {
1540                        Some(max_str.parse::<u32>().map_err(|_| SvaParseError {
1541                            message: format!("invalid delay max: '{}'", max_str),
1542                        })?)
1543                    };
1544                    return Ok(SvaExpr::Delay {
1545                        body: Box::new(parse_unary(body_str)?),
1546                        min,
1547                        max,
1548                    });
1549                }
1550            }
1551        } else {
1552            // ##N body — exact delay
1553            let mut num_end = 0;
1554            for c in rest.chars() {
1555                if c.is_ascii_digit() {
1556                    num_end += 1;
1557                } else {
1558                    break;
1559                }
1560            }
1561            if num_end > 0 {
1562                let n = rest[..num_end].parse::<u32>().map_err(|_| SvaParseError {
1563                    message: format!("invalid delay number: '{}'", &rest[..num_end]),
1564                })?;
1565                let body_str = rest[num_end..].trim();
1566                return Ok(SvaExpr::Delay {
1567                    body: Box::new(parse_unary(body_str)?),
1568                    min: n,
1569                    max: Some(n), // exact delay: min == max
1570                });
1571            }
1572        }
1573    }
1574
1575    // Negation: !(...)
1576    if input.starts_with('!') {
1577        let inner = input[1..].trim();
1578        let inner = strip_parens(inner);
1579        return Ok(SvaExpr::Not(Box::new(parse_implication(inner)?)));
1580    }
1581
1582    // IEEE 1800 system functions — $onehot0 BEFORE $onehot (prefix clarity)
1583    if let Some(result) = try_parse_function_call(input, "$onehot0", |inner| {
1584        Ok(SvaExpr::OneHot0(Box::new(parse_implication(inner)?)))
1585    })? { return Ok(result); }
1586
1587    if let Some(result) = try_parse_function_call(input, "$onehot", |inner| {
1588        Ok(SvaExpr::OneHot(Box::new(parse_implication(inner)?)))
1589    })? { return Ok(result); }
1590
1591    if let Some(result) = try_parse_function_call(input, "$countones", |inner| {
1592        Ok(SvaExpr::CountOnes(Box::new(parse_implication(inner)?)))
1593    })? { return Ok(result); }
1594
1595    if let Some(result) = try_parse_function_call(input, "$isunknown", |inner| {
1596        Ok(SvaExpr::IsUnknown(Box::new(parse_implication(inner)?)))
1597    })? { return Ok(result); }
1598
1599    if let Some(result) = try_parse_function_call(input, "$sampled", |inner| {
1600        Ok(SvaExpr::Sampled(Box::new(parse_implication(inner)?)))
1601    })? { return Ok(result); }
1602
1603    if let Some(result) = try_parse_function_call(input, "$bits", |inner| {
1604        Ok(SvaExpr::Bits(Box::new(parse_implication(inner)?)))
1605    })? { return Ok(result); }
1606
1607    if let Some(result) = try_parse_function_call(input, "$clog2", |inner| {
1608        Ok(SvaExpr::Clog2(Box::new(parse_implication(inner)?)))
1609    })? { return Ok(result); }
1610
1611    // $countbits(sig, '0', '1', ...) — generalized bit counting (IEEE 20.9)
1612    if input.starts_with("$countbits(") {
1613        if let Some(close) = find_balanced_close(input, "$countbits".len()) {
1614            let inner = &input["$countbits".len() + 1..close];
1615            // Parse: first arg is signal, rest are control chars
1616            let parts: Vec<&str> = inner.split(',').collect();
1617            if !parts.is_empty() {
1618                let sig = parse_implication(parts[0].trim())?;
1619                let mut control_chars = Vec::new();
1620                for part in &parts[1..] {
1621                    let trimmed = part.trim().trim_matches('\'');
1622                    if let Some(c) = trimmed.chars().next() {
1623                        control_chars.push(c);
1624                    }
1625                }
1626                return Ok(SvaExpr::CountBits(Box::new(sig), control_chars));
1627            }
1628        }
1629    }
1630
1631    // $isunbounded(param) — parameter bound check (IEEE 20.9)
1632    if let Some(result) = try_parse_function_call(input, "$isunbounded", |inner| {
1633        Ok(SvaExpr::IsUnbounded(Box::new(parse_implication(inner)?)))
1634    })? { return Ok(result); }
1635
1636    // $rose(...), $fell(...), $stable(...), $changed(...), s_eventually(...), $nexttime(...)
1637    // Use balanced paren matching so "$fell(sda) && scl" correctly parses $fell(sda) only
1638    if let Some(result) = try_parse_function_call(input, "$rose", |inner| {
1639        Ok(SvaExpr::Rose(Box::new(parse_implication(inner)?)))
1640    })? { return Ok(result); }
1641
1642    if let Some(result) = try_parse_function_call(input, "$fell", |inner| {
1643        Ok(SvaExpr::Fell(Box::new(parse_implication(inner)?)))
1644    })? { return Ok(result); }
1645
1646    if let Some(result) = try_parse_function_call(input, "$stable", |inner| {
1647        Ok(SvaExpr::Stable(Box::new(parse_implication(inner)?)))
1648    })? { return Ok(result); }
1649
1650    if let Some(result) = try_parse_function_call(input, "$changed", |inner| {
1651        Ok(SvaExpr::Changed(Box::new(parse_implication(inner)?)))
1652    })? { return Ok(result); }
1653
1654    if let Some(result) = try_parse_function_call(input, "s_eventually", |inner| {
1655        Ok(SvaExpr::SEventually(Box::new(parse_implication(inner)?)))
1656    })? { return Ok(result); }
1657
1658    if let Some(result) = try_parse_function_call(input, "s_always", |inner| {
1659        Ok(SvaExpr::SAlways(Box::new(parse_implication(inner)?)))
1660    })? { return Ok(result); }
1661
1662    // nexttime[N](body) — with explicit count
1663    if input.starts_with("nexttime[") {
1664        if let Some(bracket_end) = input.find(']') {
1665            let n_str = &input[9..bracket_end];
1666            if let Ok(n) = n_str.parse::<u32>() {
1667                let rest = input[bracket_end + 1..].trim();
1668                if rest.starts_with('(') {
1669                    if let Some(close) = find_balanced_close(rest, 0) {
1670                        let inner = &rest[1..close];
1671                        return Ok(SvaExpr::Nexttime(
1672                            Box::new(parse_implication(inner.trim())?),
1673                            n,
1674                        ));
1675                    }
1676                }
1677            }
1678        }
1679    }
1680
1681    // nexttime(body) — default count = 1
1682    if let Some(result) = try_parse_function_call(input, "nexttime", |inner| {
1683        Ok(SvaExpr::Nexttime(Box::new(parse_implication(inner)?), 1))
1684    })? { return Ok(result); }
1685
1686    if let Some(result) = try_parse_function_call(input, "$nexttime", |inner| {
1687        Ok(SvaExpr::Nexttime(Box::new(parse_implication(inner)?), 1))
1688    })? { return Ok(result); }
1689
1690    if let Some(result) = try_parse_function_call(input, "first_match", |inner| {
1691        Ok(SvaExpr::FirstMatch(Box::new(parse_implication(inner)?)))
1692    })? { return Ok(result); }
1693
1694    if let Some(result) = try_parse_function_call(input, "$past", |inner| {
1695        // $past(sig, n) — parse the signal and count
1696        if let Some(comma) = inner.find(',') {
1697            let sig = inner[..comma].trim();
1698            let n_str = inner[comma + 1..].trim();
1699            let n = n_str.parse::<u32>().unwrap_or(1);
1700            Ok(SvaExpr::Past(Box::new(parse_atom(sig)?), n))
1701        } else {
1702            Ok(SvaExpr::Past(Box::new(parse_atom(inner)?), 1))
1703        }
1704    })? { return Ok(result); }
1705
1706    // Parenthesized expression
1707    if input.starts_with('(') && input.ends_with(')') {
1708        return parse_implication(&input[1..input.len() - 1]);
1709    }
1710
1711    parse_atom(input)
1712}
1713
1714/// Find the `else` keyword in action block text, skipping occurrences inside
1715/// string literals (both regular `"..."` and triple-quoted `"""..."""`).
1716/// Returns the byte offset of the `else` keyword, or None if not found outside strings.
1717fn find_else_outside_strings(input: &str) -> Option<usize> {
1718    let bytes = input.as_bytes();
1719    let len = bytes.len();
1720    let mut i = 0;
1721    while i < len {
1722        // Check for triple-quoted string: """..."""
1723        if i + 2 < len && bytes[i] == b'"' && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
1724            i += 3; // skip opening """
1725            while i < len {
1726                if bytes[i] == b'\\' && i + 1 < len {
1727                    i += 2; // skip escape sequence
1728                } else if i + 2 < len && bytes[i] == b'"' && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
1729                    i += 3; // skip closing """
1730                    break;
1731                } else {
1732                    i += 1;
1733                }
1734            }
1735            continue;
1736        }
1737        // Check for regular string: "..."
1738        if bytes[i] == b'"' {
1739            i += 1; // skip opening "
1740            while i < len {
1741                if bytes[i] == b'\\' && i + 1 < len {
1742                    i += 2; // skip escape sequence
1743                } else if bytes[i] == b'"' {
1744                    i += 1; // skip closing "
1745                    break;
1746                } else {
1747                    i += 1;
1748                }
1749            }
1750            continue;
1751        }
1752        // Check for `else` keyword at word boundary
1753        if i + 4 <= len && &input[i..i + 4] == "else" {
1754            // Verify it's at a word boundary (not part of a larger identifier)
1755            let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
1756            let after_ok = i + 4 >= len || !bytes[i + 4].is_ascii_alphanumeric();
1757            if before_ok && after_ok {
1758                return Some(i);
1759            }
1760        }
1761        i += 1;
1762    }
1763    None
1764}
1765
1766/// Find the closing paren that balances the opening paren at `start`.
1767/// Returns the index of the closing ')' relative to the input string.
1768fn find_balanced_close(input: &str, start: usize) -> Option<usize> {
1769    let chars: Vec<char> = input.chars().collect();
1770    let mut depth = 0i32;
1771    for i in start..chars.len() {
1772        match chars[i] {
1773            '(' => depth += 1,
1774            ')' => {
1775                depth -= 1;
1776                if depth == 0 {
1777                    return Some(i);
1778                }
1779            }
1780            _ => {}
1781        }
1782    }
1783    None
1784}
1785
1786/// Try to parse a function call like `$rose(expr)` with balanced parens.
1787/// If the input starts with `prefix(`, extracts the balanced inner expression,
1788/// parses it with the provided closure, and returns the result.
1789/// If there's content after the closing paren, this returns None so the caller
1790/// can try parsing at a higher level (e.g., `$rose(sig) && other` should be
1791/// parsed as And($rose(sig), other) at the And level, not here).
1792fn try_parse_function_call<F>(
1793    input: &str,
1794    prefix: &str,
1795    parse_inner: F,
1796) -> Result<Option<SvaExpr>, SvaParseError>
1797where
1798    F: FnOnce(&str) -> Result<SvaExpr, SvaParseError>,
1799{
1800    let full_prefix = format!("{}(", prefix);
1801    if !input.starts_with(&full_prefix) {
1802        return Ok(None);
1803    }
1804    let paren_start = full_prefix.len() - 1; // index of '('
1805    if let Some(close) = find_balanced_close(input, paren_start) {
1806        let inner = &input[full_prefix.len()..close];
1807        let remaining = input[close + 1..].trim();
1808        if remaining.is_empty() {
1809            // Simple case: $rose(sig) with nothing after
1810            return Ok(Some(parse_inner(inner.trim())?));
1811        }
1812        // There's stuff after the closing paren (e.g., "$rose(sig) && other")
1813        // Parse just the function call, then let the caller handle the rest
1814        // We can't handle this at the unary level — return None so the
1815        // expression gets reparsed at the binary operator level.
1816        // But we need to handle it: wrap as atom.
1817        // Actually, re-parse the entire input through the binary operators:
1818        // The issue is that "$fell(sda) && scl" is at the AND level, not unary.
1819        // So we parse just "$fell(sda)" as the left side of AND.
1820        return Ok(None);
1821    }
1822    Err(SvaParseError {
1823        message: format!("unbalanced parens in {}", prefix),
1824    })
1825}
1826
1827/// Find the position of the top-level "else" keyword (not inside parens).
1828fn find_else_keyword(input: &str) -> Option<usize> {
1829    let mut depth = 0i32;
1830    let bytes = input.as_bytes();
1831    for i in 0..input.len().saturating_sub(3) {
1832        match bytes[i] {
1833            b'(' => depth += 1,
1834            b')' => depth -= 1,
1835            b'e' if depth == 0 => {
1836                if input[i..].starts_with("else") {
1837                    // Check word boundary
1838                    let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
1839                    let after_ok = i + 4 >= input.len() || !bytes[i + 4].is_ascii_alphanumeric();
1840                    if before_ok && after_ok {
1841                        return Some(i);
1842                    }
1843                }
1844            }
1845            _ => {}
1846        }
1847    }
1848    None
1849}
1850
1851fn parse_atom(input: &str) -> Result<SvaExpr, SvaParseError> {
1852    let input = input.trim();
1853    if input.is_empty() {
1854        return Err(SvaParseError {
1855            message: "empty expression".to_string(),
1856        });
1857    }
1858
1859    // Check for [+] repetition shorthand: signal[+] ≡ signal[*1:$]
1860    if let Some(bracket_pos) = input.find("[+]") {
1861        let signal_part = input[..bracket_pos].trim();
1862        let body = parse_atom(signal_part)?;
1863        return Ok(SvaExpr::Repetition {
1864            body: Box::new(body),
1865            min: 1,
1866            max: None,
1867        });
1868    }
1869
1870    // Check for repetition: signal[*N] or signal[*min:max] or signal[*]
1871    if let Some(bracket_pos) = input.find("[*") {
1872        let signal_part = input[..bracket_pos].trim();
1873        let rep_part = &input[bracket_pos + 2..];
1874        if let Some(close_bracket) = rep_part.find(']') {
1875            let range_str = &rep_part[..close_bracket].trim();
1876            // [*] shorthand ≡ [*0:$]
1877            if range_str.is_empty() {
1878                let body = parse_atom(signal_part)?;
1879                return Ok(SvaExpr::Repetition {
1880                    body: Box::new(body),
1881                    min: 0,
1882                    max: None,
1883                });
1884            }
1885            let body = parse_atom(signal_part)?;
1886            if let Some(colon) = range_str.find(':') {
1887                let min_str = range_str[..colon].trim();
1888                let max_str = range_str[colon + 1..].trim();
1889                let min = min_str.parse::<u32>().map_err(|_| SvaParseError {
1890                    message: format!("invalid repetition min: '{}'", min_str),
1891                })?;
1892                let max = if max_str == "$" {
1893                    None
1894                } else {
1895                    Some(max_str.parse::<u32>().map_err(|_| SvaParseError {
1896                        message: format!("invalid repetition max: '{}'", max_str),
1897                    })?)
1898                };
1899                return Ok(SvaExpr::Repetition {
1900                    body: Box::new(body),
1901                    min,
1902                    max,
1903                });
1904            } else {
1905                // Exact repetition: [*N]
1906                let n = range_str.trim().parse::<u32>().map_err(|_| SvaParseError {
1907                    message: format!("invalid repetition count: '{}'", range_str),
1908                })?;
1909                return Ok(SvaExpr::Repetition {
1910                    body: Box::new(body),
1911                    min: n,
1912                    max: Some(n),
1913                });
1914            }
1915        }
1916    }
1917
1918    // Goto repetition: signal[->N]
1919    if let Some(bracket_pos) = input.find("[->") {
1920        let signal_part = input[..bracket_pos].trim();
1921        let rep_part = &input[bracket_pos + 3..];
1922        if let Some(close_bracket) = rep_part.find(']') {
1923            let count_str = rep_part[..close_bracket].trim();
1924            let count = count_str.parse::<u32>().map_err(|_| SvaParseError {
1925                message: format!("invalid goto repetition count: '{}'", count_str),
1926            })?;
1927            return Ok(SvaExpr::GotoRepetition {
1928                body: Box::new(parse_atom(signal_part)?),
1929                count,
1930            });
1931        }
1932    }
1933
1934    // Non-consecutive repetition: signal[=N] or signal[=min:max] or signal[=min:$]
1935    if let Some(bracket_pos) = input.find("[=") {
1936        let signal_part = input[..bracket_pos].trim();
1937        let rep_part = &input[bracket_pos + 2..];
1938        if let Some(close_bracket) = rep_part.find(']') {
1939            let range_str = &rep_part[..close_bracket];
1940            let body = parse_atom(signal_part)?;
1941            if let Some(colon) = range_str.find(':') {
1942                let min = range_str[..colon].trim().parse::<u32>().map_err(|_| SvaParseError {
1943                    message: format!("invalid non-consec repetition min: '{}'", &range_str[..colon]),
1944                })?;
1945                let max_str = range_str[colon + 1..].trim();
1946                let max = if max_str == "$" {
1947                    None
1948                } else {
1949                    Some(max_str.parse::<u32>().map_err(|_| SvaParseError {
1950                        message: format!("invalid non-consec repetition max: '{}'", max_str),
1951                    })?)
1952                };
1953                return Ok(SvaExpr::NonConsecRepetition {
1954                    body: Box::new(body),
1955                    min,
1956                    max,
1957                });
1958            } else {
1959                let n = range_str.trim().parse::<u32>().map_err(|_| SvaParseError {
1960                    message: format!("invalid non-consec repetition count: '{}'", range_str),
1961                })?;
1962                return Ok(SvaExpr::NonConsecRepetition {
1963                    body: Box::new(body),
1964                    min: n,
1965                    max: Some(n),
1966                });
1967            }
1968        }
1969    }
1970
1971    // IEEE 1800-2023 5.7.2: Real literal constants (e.g., 1.5, 1.2E3, 1.30e-2)
1972    // Must have digit on each side of decimal point (.12 and 9. are invalid)
1973    if (input.contains('.') || input.contains('e') || input.contains('E'))
1974        && input.chars().next().map_or(false, |c| c.is_ascii_digit())
1975    {
1976        if let Ok(v) = input.parse::<f64>() {
1977            return Ok(SvaExpr::RealConst(v));
1978        }
1979    }
1980
1981    // Check if it's a number (plain or Verilog-style width'd value)
1982    if let Ok(n) = input.parse::<u64>() {
1983        return Ok(SvaExpr::Const(n, 32));
1984    }
1985    // Verilog numeric literal: N'd M or N'hXX etc.
1986    if let Some(tick_pos) = input.find('\'') {
1987        let width_str = &input[..tick_pos];
1988        let rest = &input[tick_pos + 1..];
1989        if let Ok(width) = width_str.parse::<u32>() {
1990            let (radix, value_str) = if rest.starts_with('d') || rest.starts_with('D') {
1991                (10, &rest[1..])
1992            } else if rest.starts_with('h') || rest.starts_with('H') {
1993                (16, &rest[1..])
1994            } else if rest.starts_with('b') || rest.starts_with('B') {
1995                (2, &rest[1..])
1996            } else if rest.starts_with('o') || rest.starts_with('O') {
1997                (8, &rest[1..])
1998            } else {
1999                (10, rest)
2000            };
2001            if let Ok(value) = u64::from_str_radix(value_str, radix) {
2002                return Ok(SvaExpr::Const(value, width));
2003            }
2004        }
2005    }
2006
2007    // IEEE 1800-2023: type(this) construct (IEEE 6.23)
2008    if input == "type(this)" {
2009        return Ok(SvaExpr::TypeThis);
2010    }
2011
2012    // IEEE 1800-2023: A.map(x) with (expr) — array map method (IEEE 7.12)
2013    if let Some(dot_map_pos) = input.find(".map(") {
2014        let array_part = &input[..dot_map_pos].trim();
2015        let after_map = &input[dot_map_pos + 5..]; // skip ".map("
2016        // Find closing paren for iterator args
2017        if let Some(iter_close) = after_map.find(')') {
2018            let iter_part = after_map[..iter_close].trim();
2019            let iterator = if iter_part.is_empty() {
2020                "item".to_string() // default iterator name per IEEE 7.12
2021            } else {
2022                // May have iterator and optional index arg: "x" or "x, i"
2023                iter_part.split(',').next().unwrap_or("item").trim().to_string()
2024            };
2025            let after_iter_close = after_map[iter_close + 1..].trim();
2026            // Expect `with (expr)`
2027            if after_iter_close.starts_with("with") {
2028                let with_body = after_iter_close["with".len()..].trim();
2029                let with_expr_str = strip_parens(with_body);
2030                let array_expr = parse_atom(array_part)?;
2031                let with_expr = parse_sva(with_expr_str)?;
2032                return Ok(SvaExpr::ArrayMap {
2033                    array: Box::new(array_expr),
2034                    iterator,
2035                    with_expr: Box::new(with_expr),
2036                });
2037            }
2038        }
2039    }
2040
2041    // Must be a signal name
2042    if input
2043        .chars()
2044        .all(|c| c.is_alphanumeric() || c == '_')
2045    {
2046        return Ok(SvaExpr::Signal(input.to_string()));
2047    }
2048
2049    Err(SvaParseError {
2050        message: format!("unexpected token: '{}'", input),
2051    })
2052}
2053
2054/// Render an SvaExpr back to valid SVA text.
2055/// Closes the round-trip: parse_sva(text) → SvaExpr → sva_expr_to_string → text.
2056pub fn sva_expr_to_string(expr: &SvaExpr) -> String {
2057    match expr {
2058        SvaExpr::Signal(name) => name.clone(),
2059        SvaExpr::Const(value, width) => format!("{}'d{}", width, value),
2060        SvaExpr::Rose(inner) => format!("$rose({})", sva_expr_to_string(inner)),
2061        SvaExpr::Fell(inner) => format!("$fell({})", sva_expr_to_string(inner)),
2062        SvaExpr::Past(inner, n) => format!("$past({}, {})", sva_expr_to_string(inner), n),
2063        SvaExpr::And(left, right) => {
2064            format!("({} && {})", sva_expr_to_string(left), sva_expr_to_string(right))
2065        }
2066        SvaExpr::Or(left, right) => {
2067            format!("({} || {})", sva_expr_to_string(left), sva_expr_to_string(right))
2068        }
2069        SvaExpr::Not(inner) => format!("!({})", sva_expr_to_string(inner)),
2070        SvaExpr::Eq(left, right) => {
2071            format!("({} == {})", sva_expr_to_string(left), sva_expr_to_string(right))
2072        }
2073        SvaExpr::Implication {
2074            antecedent,
2075            consequent,
2076            overlapping,
2077        } => {
2078            let op = if *overlapping { "|->" } else { "|=>" };
2079            format!(
2080                "{} {} {}",
2081                sva_expr_to_string(antecedent),
2082                op,
2083                sva_expr_to_string(consequent)
2084            )
2085        }
2086        SvaExpr::Delay { body, min, max } => match (min, max) {
2087            // Unified convention: None = unbounded ($), Some(n) = bounded
2088            (0, None) => format!("##[*] {}", sva_expr_to_string(body)),
2089            (1, None) => format!("##[+] {}", sva_expr_to_string(body)),
2090            (_, None) => format!("##[{}:$] {}", min, sva_expr_to_string(body)),
2091            (_, Some(max_val)) if min == max_val => format!("##{} {}", min, sva_expr_to_string(body)),
2092            (_, Some(max_val)) => format!("##[{}:{}] {}", min, max_val, sva_expr_to_string(body)),
2093        },
2094        SvaExpr::Repetition { body, min, max } => {
2095            let body_str = sva_expr_to_string(body);
2096            match (min, max) {
2097                (0, None) => format!("{}[*]", body_str),
2098                (1, None) => format!("{}[+]", body_str),
2099                (_, Some(m)) if *m == *min => format!("{}[*{}]", body_str, min),
2100                (_, Some(m)) => format!("{}[*{}:{}]", body_str, min, m),
2101                (_, None) => format!("{}[*{}:$]", body_str, min),
2102            }
2103        }
2104        SvaExpr::SEventually(inner) => format!("s_eventually({})", sva_expr_to_string(inner)),
2105        SvaExpr::SAlways(inner) => format!("s_always({})", sva_expr_to_string(inner)),
2106        SvaExpr::Stable(inner) => format!("$stable({})", sva_expr_to_string(inner)),
2107        SvaExpr::Changed(inner) => format!("$changed({})", sva_expr_to_string(inner)),
2108        SvaExpr::Nexttime(inner, n) => {
2109            if *n == 1 {
2110                format!("nexttime({})", sva_expr_to_string(inner))
2111            } else {
2112                format!("nexttime[{}]({})", n, sva_expr_to_string(inner))
2113            }
2114        }
2115        SvaExpr::DisableIff { condition, body } => {
2116            format!("disable iff ({}) {}", sva_expr_to_string(condition), sva_expr_to_string(body))
2117        }
2118        SvaExpr::IfElse { condition, then_expr, else_expr } => {
2119            format!(
2120                "if ({}) {} else {}",
2121                sva_expr_to_string(condition),
2122                sva_expr_to_string(then_expr),
2123                sva_expr_to_string(else_expr),
2124            )
2125        }
2126        // IEEE 1800 extended (Sprint 1B)
2127        SvaExpr::NotEq(l, r) => format!("({} != {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2128        SvaExpr::LessThan(l, r) => format!("({} < {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2129        SvaExpr::GreaterThan(l, r) => format!("({} > {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2130        SvaExpr::LessEqual(l, r) => format!("({} <= {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2131        SvaExpr::GreaterEqual(l, r) => format!("({} >= {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2132        SvaExpr::Ternary { condition, then_expr, else_expr } => {
2133            format!("{} ? {} : {}",
2134                sva_expr_to_string(condition),
2135                sva_expr_to_string(then_expr),
2136                sva_expr_to_string(else_expr),
2137            )
2138        }
2139        SvaExpr::Throughout { signal, sequence } => {
2140            format!("{} throughout ({})",
2141                sva_expr_to_string(signal),
2142                sva_expr_to_string(sequence),
2143            )
2144        }
2145        SvaExpr::Within { inner, outer } => {
2146            format!("({}) within ({})",
2147                sva_expr_to_string(inner),
2148                sva_expr_to_string(outer),
2149            )
2150        }
2151        SvaExpr::FirstMatch(inner) => format!("first_match({})", sva_expr_to_string(inner)),
2152        SvaExpr::Intersect { left, right } => {
2153            format!("({}) intersect ({})",
2154                sva_expr_to_string(left),
2155                sva_expr_to_string(right),
2156            )
2157        }
2158        // IEEE 1800 system functions (Audit)
2159        SvaExpr::OneHot0(inner) => format!("$onehot0({})", sva_expr_to_string(inner)),
2160        SvaExpr::OneHot(inner) => format!("$onehot({})", sva_expr_to_string(inner)),
2161        SvaExpr::CountOnes(inner) => format!("$countones({})", sva_expr_to_string(inner)),
2162        SvaExpr::IsUnknown(inner) => format!("$isunknown({})", sva_expr_to_string(inner)),
2163        SvaExpr::Sampled(inner) => format!("$sampled({})", sva_expr_to_string(inner)),
2164        SvaExpr::Bits(inner) => format!("$bits({})", sva_expr_to_string(inner)),
2165        SvaExpr::Clog2(inner) => format!("$clog2({})", sva_expr_to_string(inner)),
2166        SvaExpr::CountBits(inner, chars) => {
2167            let char_args: Vec<String> = chars.iter().map(|c| format!("'{}'", c)).collect();
2168            format!("$countbits({}, {})", sva_expr_to_string(inner), char_args.join(", "))
2169        }
2170        SvaExpr::IsUnbounded(inner) => format!("$isunbounded({})", sva_expr_to_string(inner)),
2171        // Advanced sequences (Audit)
2172        SvaExpr::GotoRepetition { body, count } => {
2173            format!("{}[->{}]", sva_expr_to_string(body), count)
2174        }
2175        SvaExpr::NonConsecRepetition { body, min, max } => {
2176            let body_str = sva_expr_to_string(body);
2177            match max {
2178                Some(m) if *m == *min => format!("{}[={}]", body_str, min),
2179                Some(m) => format!("{}[={}:{}]", body_str, min, m),
2180                None => format!("{}[={}:$]", body_str, min),
2181            }
2182        }
2183        // Property abort operators (Audit)
2184        SvaExpr::AcceptOn { condition, body } => {
2185            format!("accept_on({}) {}", sva_expr_to_string(condition), sva_expr_to_string(body))
2186        }
2187        SvaExpr::RejectOn { condition, body } => {
2188            format!("reject_on({}) {}", sva_expr_to_string(condition), sva_expr_to_string(body))
2189        }
2190        // Property connectives (Sprint 1, IEEE 16.12.3-8)
2191        SvaExpr::PropertyNot(inner) => format!("not {}", sva_expr_to_string(inner)),
2192        SvaExpr::PropertyImplies(l, r) => {
2193            format!("{} implies {}", sva_expr_to_string(l), sva_expr_to_string(r))
2194        }
2195        SvaExpr::PropertyIff(l, r) => {
2196            format!("{} iff {}", sva_expr_to_string(l), sva_expr_to_string(r))
2197        }
2198        // LTL temporal operators (Sprint 2)
2199        SvaExpr::Always(inner) => format!("always({})", sva_expr_to_string(inner)),
2200        SvaExpr::AlwaysBounded { body, min, max } => match max {
2201            Some(m) => format!("always [{}:{}] {}", min, m, sva_expr_to_string(body)),
2202            None => format!("always [{}:$] {}", min, sva_expr_to_string(body)),
2203        },
2204        SvaExpr::SAlwaysBounded { body, min, max } => {
2205            format!("s_always [{}:{}] {}", min, max, sva_expr_to_string(body))
2206        }
2207        SvaExpr::EventuallyBounded { body, min, max } => {
2208            format!("eventually [{}:{}] {}", min, max, sva_expr_to_string(body))
2209        }
2210        SvaExpr::SEventuallyBounded { body, min, max } => match max {
2211            Some(m) => format!("s_eventually [{}:{}] {}", min, m, sva_expr_to_string(body)),
2212            None => format!("s_eventually [{}:$] {}", min, sva_expr_to_string(body)),
2213        },
2214        SvaExpr::Until { lhs, rhs, strong, inclusive } => {
2215            let op = match (strong, inclusive) {
2216                (false, false) => "until",
2217                (true, false) => "s_until",
2218                (false, true) => "until_with",
2219                (true, true) => "s_until_with",
2220            };
2221            format!("{} {} {}", sva_expr_to_string(lhs), op, sva_expr_to_string(rhs))
2222        }
2223        // Sprint 3
2224        SvaExpr::Strong(inner) => format!("strong({})", sva_expr_to_string(inner)),
2225        SvaExpr::Weak(inner) => format!("weak({})", sva_expr_to_string(inner)),
2226        SvaExpr::SNexttime(inner, n) => {
2227            if *n == 1 {
2228                format!("s_nexttime({})", sva_expr_to_string(inner))
2229            } else {
2230                format!("s_nexttime[{}]({})", n, sva_expr_to_string(inner))
2231            }
2232        }
2233        SvaExpr::FollowedBy { antecedent, consequent, overlapping } => {
2234            let op = if *overlapping { "#-#" } else { "#=#" };
2235            format!("{} {} {}", sva_expr_to_string(antecedent), op, sva_expr_to_string(consequent))
2236        }
2237        SvaExpr::PropertyCase { expression, items, default } => {
2238            let mut s = format!("case({})", sva_expr_to_string(expression));
2239            for (vals, prop) in items {
2240                let vs: Vec<String> = vals.iter().map(sva_expr_to_string).collect();
2241                s.push_str(&format!(" {}: {};", vs.join(", "), sva_expr_to_string(prop)));
2242            }
2243            if let Some(d) = default {
2244                s.push_str(&format!(" default: {};", sva_expr_to_string(d)));
2245            }
2246            s.push_str(" endcase");
2247            s
2248        }
2249        SvaExpr::SyncAcceptOn { condition, body } => {
2250            format!("sync_accept_on({}) {}", sva_expr_to_string(condition), sva_expr_to_string(body))
2251        }
2252        SvaExpr::SyncRejectOn { condition, body } => {
2253            format!("sync_reject_on({}) {}", sva_expr_to_string(condition), sva_expr_to_string(body))
2254        }
2255        // Sprint 5
2256        SvaExpr::SequenceAnd(l, r) => {
2257            format!("({}) and ({})", sva_expr_to_string(l), sva_expr_to_string(r))
2258        }
2259        SvaExpr::SequenceOr(l, r) => {
2260            format!("({}) or ({})", sva_expr_to_string(l), sva_expr_to_string(r))
2261        }
2262        // Sprint 7
2263        SvaExpr::ImmediateAssert { expression, deferred } => {
2264            match deferred {
2265                None => format!("assert({})", sva_expr_to_string(expression)),
2266                Some(ImmediateDeferred::Observed) => format!("assert #0({})", sva_expr_to_string(expression)),
2267                Some(ImmediateDeferred::Final) => format!("assert final({})", sva_expr_to_string(expression)),
2268            }
2269        }
2270        // Sprint 13
2271        SvaExpr::FieldAccess { signal, field } => format!("{}.{}", sva_expr_to_string(signal), field),
2272        SvaExpr::EnumLiteral { type_name: Some(t), value } => format!("{}::{}", t, value),
2273        SvaExpr::EnumLiteral { type_name: None, value } => value.clone(),
2274        // Sprint 14
2275        SvaExpr::Triggered(name) => format!("{}.triggered", name),
2276        SvaExpr::Matched(name) => format!("{}.matched", name),
2277        // Sprint 15
2278        SvaExpr::BitAnd(l, r) => format!("({} & {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2279        SvaExpr::BitOr(l, r) => format!("({} | {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2280        SvaExpr::BitXor(l, r) => format!("({} ^ {})", sva_expr_to_string(l), sva_expr_to_string(r)),
2281        SvaExpr::BitNot(inner) => format!("~{}", sva_expr_to_string(inner)),
2282        SvaExpr::ReductionAnd(inner) => format!("&{}", sva_expr_to_string(inner)),
2283        SvaExpr::ReductionOr(inner) => format!("|{}", sva_expr_to_string(inner)),
2284        SvaExpr::ReductionXor(inner) => format!("^{}", sva_expr_to_string(inner)),
2285        SvaExpr::BitSelect { signal, index } => format!("{}[{}]", sva_expr_to_string(signal), sva_expr_to_string(index)),
2286        SvaExpr::PartSelect { signal, high, low } => format!("{}[{}:{}]", sva_expr_to_string(signal), high, low),
2287        SvaExpr::Concat(items) => {
2288            let parts: Vec<String> = items.iter().map(sva_expr_to_string).collect();
2289            format!("{{{}}}", parts.join(", "))
2290        }
2291        // Sprint 18
2292        SvaExpr::SequenceAction { expression, assignments } => {
2293            let assigns: Vec<String> = assignments.iter()
2294                .map(|(name, rhs)| format!("{} = {}", name, sva_expr_to_string(rhs)))
2295                .collect();
2296            format!("({}, {})", sva_expr_to_string(expression), assigns.join(", "))
2297        }
2298        SvaExpr::LocalVar(name) => name.clone(),
2299        SvaExpr::ConstCast(inner) => format!("const'({})", sva_expr_to_string(inner)),
2300        SvaExpr::Clocked { clock, edge, body } => {
2301            let edge_str = match edge {
2302                ClockEdge::Posedge => "posedge",
2303                ClockEdge::Negedge => "negedge",
2304                ClockEdge::Edge => "edge",
2305            };
2306            format!("@({} {}) {}", edge_str, clock, sva_expr_to_string(body))
2307        }
2308        // Sprint 23 (IEEE 1800-2023)
2309        SvaExpr::ArrayMap { array, iterator, with_expr } => {
2310            format!("{}.map({}) with ({})", sva_expr_to_string(array), iterator, sva_expr_to_string(with_expr))
2311        }
2312        SvaExpr::TypeThis => "type(this)".to_string(),
2313        SvaExpr::RealConst(v) => format!("{}", v),
2314    }
2315}
2316
2317fn strip_parens(input: &str) -> &str {
2318    let input = input.trim();
2319    if input.starts_with('(') && input.ends_with(')') {
2320        &input[1..input.len() - 1]
2321    } else {
2322        input
2323    }
2324}
2325
2326/// Check if two SvaExpr trees are structurally equivalent.
2327pub fn sva_exprs_structurally_equivalent(a: &SvaExpr, b: &SvaExpr) -> bool {
2328    match (a, b) {
2329        (SvaExpr::Signal(sa), SvaExpr::Signal(sb)) => sa == sb,
2330        (SvaExpr::Const(va, wa), SvaExpr::Const(vb, wb)) => va == vb && wa == wb,
2331        (SvaExpr::Rose(ia), SvaExpr::Rose(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2332        (SvaExpr::Fell(ia), SvaExpr::Fell(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2333        (SvaExpr::Past(ia, na), SvaExpr::Past(ib, nb)) => {
2334            na == nb && sva_exprs_structurally_equivalent(ia, ib)
2335        }
2336        (SvaExpr::And(la, ra), SvaExpr::And(lb, rb)) => {
2337            sva_exprs_structurally_equivalent(la, lb)
2338                && sva_exprs_structurally_equivalent(ra, rb)
2339        }
2340        (SvaExpr::Or(la, ra), SvaExpr::Or(lb, rb)) => {
2341            sva_exprs_structurally_equivalent(la, lb)
2342                && sva_exprs_structurally_equivalent(ra, rb)
2343        }
2344        (SvaExpr::Not(ia), SvaExpr::Not(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2345        (SvaExpr::Eq(la, ra), SvaExpr::Eq(lb, rb)) => {
2346            sva_exprs_structurally_equivalent(la, lb)
2347                && sva_exprs_structurally_equivalent(ra, rb)
2348        }
2349        (
2350            SvaExpr::Implication {
2351                antecedent: aa,
2352                consequent: ca,
2353                overlapping: oa,
2354            },
2355            SvaExpr::Implication {
2356                antecedent: ab,
2357                consequent: cb,
2358                overlapping: ob,
2359            },
2360        ) => {
2361            oa == ob
2362                && sva_exprs_structurally_equivalent(aa, ab)
2363                && sva_exprs_structurally_equivalent(ca, cb)
2364        }
2365        (
2366            SvaExpr::Delay {
2367                body: ba,
2368                min: mna,
2369                max: mxa,
2370            },
2371            SvaExpr::Delay {
2372                body: bb,
2373                min: mnb,
2374                max: mxb,
2375            },
2376        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2377        (
2378            SvaExpr::Repetition { body: ba, min: mna, max: mxa },
2379            SvaExpr::Repetition { body: bb, min: mnb, max: mxb },
2380        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2381        (SvaExpr::SEventually(ia), SvaExpr::SEventually(ib)) => {
2382            sva_exprs_structurally_equivalent(ia, ib)
2383        }
2384        (SvaExpr::SAlways(ia), SvaExpr::SAlways(ib)) => {
2385            sva_exprs_structurally_equivalent(ia, ib)
2386        }
2387        (SvaExpr::Stable(ia), SvaExpr::Stable(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2388        (SvaExpr::Changed(ia), SvaExpr::Changed(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2389        (SvaExpr::Nexttime(ia, na), SvaExpr::Nexttime(ib, nb)) => {
2390            na == nb && sva_exprs_structurally_equivalent(ia, ib)
2391        }
2392        (
2393            SvaExpr::DisableIff { condition: ca, body: ba },
2394            SvaExpr::DisableIff { condition: cb, body: bb },
2395        ) => {
2396            sva_exprs_structurally_equivalent(ca, cb)
2397                && sva_exprs_structurally_equivalent(ba, bb)
2398        }
2399        (
2400            SvaExpr::IfElse { condition: ca, then_expr: ta, else_expr: ea },
2401            SvaExpr::IfElse { condition: cb, then_expr: tb, else_expr: eb },
2402        ) => {
2403            sva_exprs_structurally_equivalent(ca, cb)
2404                && sva_exprs_structurally_equivalent(ta, tb)
2405                && sva_exprs_structurally_equivalent(ea, eb)
2406        }
2407        // IEEE 1800 extended (Sprint 1B)
2408        (SvaExpr::NotEq(la, ra), SvaExpr::NotEq(lb, rb)) => {
2409            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2410        }
2411        (SvaExpr::LessThan(la, ra), SvaExpr::LessThan(lb, rb)) => {
2412            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2413        }
2414        (SvaExpr::GreaterThan(la, ra), SvaExpr::GreaterThan(lb, rb)) => {
2415            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2416        }
2417        (SvaExpr::LessEqual(la, ra), SvaExpr::LessEqual(lb, rb)) => {
2418            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2419        }
2420        (SvaExpr::GreaterEqual(la, ra), SvaExpr::GreaterEqual(lb, rb)) => {
2421            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2422        }
2423        (
2424            SvaExpr::Ternary { condition: ca, then_expr: ta, else_expr: ea },
2425            SvaExpr::Ternary { condition: cb, then_expr: tb, else_expr: eb },
2426        ) => {
2427            sva_exprs_structurally_equivalent(ca, cb)
2428                && sva_exprs_structurally_equivalent(ta, tb)
2429                && sva_exprs_structurally_equivalent(ea, eb)
2430        }
2431        (
2432            SvaExpr::Throughout { signal: sa, sequence: qa },
2433            SvaExpr::Throughout { signal: sb, sequence: qb },
2434        ) => {
2435            sva_exprs_structurally_equivalent(sa, sb) && sva_exprs_structurally_equivalent(qa, qb)
2436        }
2437        (
2438            SvaExpr::Within { inner: ia, outer: oa },
2439            SvaExpr::Within { inner: ib, outer: ob },
2440        ) => {
2441            sva_exprs_structurally_equivalent(ia, ib) && sva_exprs_structurally_equivalent(oa, ob)
2442        }
2443        (SvaExpr::FirstMatch(ia), SvaExpr::FirstMatch(ib)) => {
2444            sva_exprs_structurally_equivalent(ia, ib)
2445        }
2446        (
2447            SvaExpr::Intersect { left: la, right: ra },
2448            SvaExpr::Intersect { left: lb, right: rb },
2449        ) => {
2450            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2451        }
2452        // IEEE 1800 system functions (Audit)
2453        (SvaExpr::OneHot0(ia), SvaExpr::OneHot0(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2454        (SvaExpr::OneHot(ia), SvaExpr::OneHot(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2455        (SvaExpr::CountOnes(ia), SvaExpr::CountOnes(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2456        (SvaExpr::IsUnknown(ia), SvaExpr::IsUnknown(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2457        (SvaExpr::Sampled(ia), SvaExpr::Sampled(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2458        (SvaExpr::Bits(ia), SvaExpr::Bits(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2459        (SvaExpr::Clog2(ia), SvaExpr::Clog2(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2460        (SvaExpr::CountBits(ia, ca), SvaExpr::CountBits(ib, cb)) =>
2461            ca == cb && sva_exprs_structurally_equivalent(ia, ib),
2462        (SvaExpr::IsUnbounded(ia), SvaExpr::IsUnbounded(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2463        // Advanced sequences (Audit)
2464        (
2465            SvaExpr::GotoRepetition { body: ba, count: ca },
2466            SvaExpr::GotoRepetition { body: bb, count: cb },
2467        ) => ca == cb && sva_exprs_structurally_equivalent(ba, bb),
2468        (
2469            SvaExpr::NonConsecRepetition { body: ba, min: mna, max: mxa },
2470            SvaExpr::NonConsecRepetition { body: bb, min: mnb, max: mxb },
2471        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2472        // Property abort operators (Audit)
2473        (
2474            SvaExpr::AcceptOn { condition: ca, body: ba },
2475            SvaExpr::AcceptOn { condition: cb, body: bb },
2476        ) => {
2477            sva_exprs_structurally_equivalent(ca, cb)
2478                && sva_exprs_structurally_equivalent(ba, bb)
2479        }
2480        (
2481            SvaExpr::RejectOn { condition: ca, body: ba },
2482            SvaExpr::RejectOn { condition: cb, body: bb },
2483        ) => {
2484            sva_exprs_structurally_equivalent(ca, cb)
2485                && sva_exprs_structurally_equivalent(ba, bb)
2486        }
2487        // Property connectives (Sprint 1)
2488        (SvaExpr::PropertyNot(ia), SvaExpr::PropertyNot(ib)) => {
2489            sva_exprs_structurally_equivalent(ia, ib)
2490        }
2491        (SvaExpr::PropertyImplies(la, ra), SvaExpr::PropertyImplies(lb, rb)) => {
2492            sva_exprs_structurally_equivalent(la, lb)
2493                && sva_exprs_structurally_equivalent(ra, rb)
2494        }
2495        (SvaExpr::PropertyIff(la, ra), SvaExpr::PropertyIff(lb, rb)) => {
2496            sva_exprs_structurally_equivalent(la, lb)
2497                && sva_exprs_structurally_equivalent(ra, rb)
2498        }
2499        // LTL temporal operators (Sprint 2)
2500        (SvaExpr::Always(ia), SvaExpr::Always(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2501        (
2502            SvaExpr::AlwaysBounded { body: ba, min: mna, max: mxa },
2503            SvaExpr::AlwaysBounded { body: bb, min: mnb, max: mxb },
2504        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2505        (
2506            SvaExpr::SAlwaysBounded { body: ba, min: mna, max: mxa },
2507            SvaExpr::SAlwaysBounded { body: bb, min: mnb, max: mxb },
2508        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2509        (
2510            SvaExpr::EventuallyBounded { body: ba, min: mna, max: mxa },
2511            SvaExpr::EventuallyBounded { body: bb, min: mnb, max: mxb },
2512        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2513        (
2514            SvaExpr::SEventuallyBounded { body: ba, min: mna, max: mxa },
2515            SvaExpr::SEventuallyBounded { body: bb, min: mnb, max: mxb },
2516        ) => mna == mnb && mxa == mxb && sva_exprs_structurally_equivalent(ba, bb),
2517        (
2518            SvaExpr::Until { lhs: la, rhs: ra, strong: sa, inclusive: ia },
2519            SvaExpr::Until { lhs: lb, rhs: rb, strong: sb, inclusive: ib },
2520        ) => {
2521            sa == sb && ia == ib
2522                && sva_exprs_structurally_equivalent(la, lb)
2523                && sva_exprs_structurally_equivalent(ra, rb)
2524        }
2525        // Sprint 3
2526        (SvaExpr::Strong(ia), SvaExpr::Strong(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2527        (SvaExpr::Weak(ia), SvaExpr::Weak(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2528        (SvaExpr::SNexttime(ia, na), SvaExpr::SNexttime(ib, nb)) => {
2529            na == nb && sva_exprs_structurally_equivalent(ia, ib)
2530        }
2531        (
2532            SvaExpr::FollowedBy { antecedent: aa, consequent: ca, overlapping: oa },
2533            SvaExpr::FollowedBy { antecedent: ab, consequent: cb, overlapping: ob },
2534        ) => {
2535            oa == ob
2536                && sva_exprs_structurally_equivalent(aa, ab)
2537                && sva_exprs_structurally_equivalent(ca, cb)
2538        }
2539        (
2540            SvaExpr::PropertyCase { expression: ea, items: ia, default: da },
2541            SvaExpr::PropertyCase { expression: eb, items: ib, default: db },
2542        ) => {
2543            sva_exprs_structurally_equivalent(ea, eb)
2544                && ia.len() == ib.len()
2545                && ia.iter().zip(ib.iter()).all(|((va, pa), (vb, pb))| {
2546                    va.len() == vb.len()
2547                        && va.iter().zip(vb.iter()).all(|(a, b)| sva_exprs_structurally_equivalent(a, b))
2548                        && sva_exprs_structurally_equivalent(pa, pb)
2549                })
2550                && match (da, db) {
2551                    (Some(a), Some(b)) => sva_exprs_structurally_equivalent(a, b),
2552                    (None, None) => true,
2553                    _ => false,
2554                }
2555        }
2556        (
2557            SvaExpr::SyncAcceptOn { condition: ca, body: ba },
2558            SvaExpr::SyncAcceptOn { condition: cb, body: bb },
2559        ) => {
2560            sva_exprs_structurally_equivalent(ca, cb)
2561                && sva_exprs_structurally_equivalent(ba, bb)
2562        }
2563        (
2564            SvaExpr::SyncRejectOn { condition: ca, body: ba },
2565            SvaExpr::SyncRejectOn { condition: cb, body: bb },
2566        ) => {
2567            sva_exprs_structurally_equivalent(ca, cb)
2568                && sva_exprs_structurally_equivalent(ba, bb)
2569        }
2570        // Sprint 5
2571        (SvaExpr::SequenceAnd(la, ra), SvaExpr::SequenceAnd(lb, rb)) => {
2572            sva_exprs_structurally_equivalent(la, lb)
2573                && sva_exprs_structurally_equivalent(ra, rb)
2574        }
2575        (SvaExpr::SequenceOr(la, ra), SvaExpr::SequenceOr(lb, rb)) => {
2576            sva_exprs_structurally_equivalent(la, lb)
2577                && sva_exprs_structurally_equivalent(ra, rb)
2578        }
2579        // Sprint 7
2580        (
2581            SvaExpr::ImmediateAssert { expression: ea, deferred: da },
2582            SvaExpr::ImmediateAssert { expression: eb, deferred: db },
2583        ) => da == db && sva_exprs_structurally_equivalent(ea, eb),
2584        // Sprint 13
2585        (SvaExpr::FieldAccess { signal: sa, field: fa }, SvaExpr::FieldAccess { signal: sb, field: fb }) => {
2586            fa == fb && sva_exprs_structurally_equivalent(sa, sb)
2587        }
2588        (SvaExpr::EnumLiteral { type_name: ta, value: va }, SvaExpr::EnumLiteral { type_name: tb, value: vb }) => {
2589            ta == tb && va == vb
2590        }
2591        // Sprint 14
2592        (SvaExpr::Triggered(a), SvaExpr::Triggered(b)) => a == b,
2593        (SvaExpr::Matched(a), SvaExpr::Matched(b)) => a == b,
2594        // Sprint 15
2595        (SvaExpr::BitAnd(la, ra), SvaExpr::BitAnd(lb, rb)) => {
2596            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2597        }
2598        (SvaExpr::BitOr(la, ra), SvaExpr::BitOr(lb, rb)) => {
2599            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2600        }
2601        (SvaExpr::BitXor(la, ra), SvaExpr::BitXor(lb, rb)) => {
2602            sva_exprs_structurally_equivalent(la, lb) && sva_exprs_structurally_equivalent(ra, rb)
2603        }
2604        (SvaExpr::BitNot(ia), SvaExpr::BitNot(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2605        (SvaExpr::ReductionAnd(ia), SvaExpr::ReductionAnd(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2606        (SvaExpr::ReductionOr(ia), SvaExpr::ReductionOr(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2607        (SvaExpr::ReductionXor(ia), SvaExpr::ReductionXor(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2608        (SvaExpr::BitSelect { signal: sa, index: ia }, SvaExpr::BitSelect { signal: sb, index: ib }) => {
2609            sva_exprs_structurally_equivalent(sa, sb) && sva_exprs_structurally_equivalent(ia, ib)
2610        }
2611        (SvaExpr::PartSelect { signal: sa, high: ha, low: la }, SvaExpr::PartSelect { signal: sb, high: hb, low: lb }) => {
2612            ha == hb && la == lb && sva_exprs_structurally_equivalent(sa, sb)
2613        }
2614        (SvaExpr::Concat(ia), SvaExpr::Concat(ib)) => {
2615            ia.len() == ib.len() && ia.iter().zip(ib.iter()).all(|(a, b)| sva_exprs_structurally_equivalent(a, b))
2616        }
2617        // Sprint 18
2618        (SvaExpr::SequenceAction { expression: ea, assignments: aa },
2619         SvaExpr::SequenceAction { expression: eb, assignments: ab }) => {
2620            sva_exprs_structurally_equivalent(ea, eb)
2621                && aa.len() == ab.len()
2622                && aa.iter().zip(ab.iter()).all(|((na, ra), (nb, rb))|
2623                    na == nb && sva_exprs_structurally_equivalent(ra, rb))
2624        }
2625        (SvaExpr::LocalVar(a), SvaExpr::LocalVar(b)) => a == b,
2626        (SvaExpr::ConstCast(ia), SvaExpr::ConstCast(ib)) => sva_exprs_structurally_equivalent(ia, ib),
2627        (SvaExpr::Clocked { clock: ca, edge: ea, body: ba },
2628         SvaExpr::Clocked { clock: cb, edge: eb, body: bb }) => {
2629            ca == cb && ea == eb && sva_exprs_structurally_equivalent(ba, bb)
2630        }
2631        // Sprint 23 (IEEE 1800-2023)
2632        (SvaExpr::ArrayMap { array: aa, iterator: ia, with_expr: wa },
2633         SvaExpr::ArrayMap { array: ab, iterator: ib, with_expr: wb }) => {
2634            ia == ib && sva_exprs_structurally_equivalent(aa, ab) && sva_exprs_structurally_equivalent(wa, wb)
2635        }
2636        (SvaExpr::TypeThis, SvaExpr::TypeThis) => true,
2637        (SvaExpr::RealConst(a), SvaExpr::RealConst(b)) => a.to_bits() == b.to_bits(),
2638        _ => false,
2639    }
2640}
2641
2642// ═══════════════════════════════════════════════════════════════════════════
2643// Elaboration Pass — Default Clocking & Disable Iff (IEEE 16.15-16)
2644// ═══════════════════════════════════════════════════════════════════════════
2645
2646/// Context for elaborating directives with default clocking and disable iff.
2647/// IEEE 16.15: `default clocking` applies to all assertions in scope that
2648/// lack an explicit clock. IEEE 16.16: `default disable iff` applies the
2649/// reset condition to all assertions lacking explicit `disable iff`.
2650#[derive(Debug, Clone, Default)]
2651pub struct ElaborationContext {
2652    pub default_clocking: Option<String>,
2653    pub default_disable_iff: Option<SvaExpr>,
2654}
2655
2656/// Elaborate a set of directives by applying default clocking and disable iff.
2657///
2658/// IEEE 16.15-16: For each directive:
2659/// - If it lacks an explicit clock and a default clocking exists, apply the default.
2660/// - If it lacks an explicit disable_iff and a default disable_iff exists, wrap
2661///   the property body in `DisableIff { condition, body }`.
2662/// - Explicit annotations override defaults (no clobbering).
2663pub fn elaborate_directives(
2664    directives: &[SvaDirective],
2665    ctx: &ElaborationContext,
2666) -> Vec<SvaDirective> {
2667    directives.iter().map(|d| {
2668        let mut elaborated = d.clone();
2669
2670        // Apply default clock if missing
2671        if elaborated.clock.is_none() {
2672            if let Some(ref default_clk) = ctx.default_clocking {
2673                elaborated.clock = Some(default_clk.clone());
2674            }
2675        }
2676
2677        // Apply default disable iff if missing
2678        if elaborated.disable_iff.is_none() {
2679            if let Some(ref default_dis) = ctx.default_disable_iff {
2680                elaborated.disable_iff = Some(default_dis.clone());
2681            }
2682        }
2683
2684        elaborated
2685    }).collect()
2686}
2687
2688/// Resolve `let` declarations by inlining expression substitution.
2689/// IEEE 11.12: `let` is pure expression substitution with no temporal semantics.
2690pub fn resolve_let_instance(
2691    decls: &[LetDecl],
2692    name: &str,
2693    args: &[SvaExpr],
2694) -> Result<SvaExpr, SvaParseError> {
2695    let decl = decls.iter().find(|d| d.name == name).ok_or_else(|| SvaParseError {
2696        message: format!("undeclared let: '{}'", name),
2697    })?;
2698
2699    let required_count = decl.ports.iter().filter(|p| p.default.is_none()).count();
2700    if args.len() < required_count || args.len() > decl.ports.len() {
2701        return Err(SvaParseError {
2702            message: format!(
2703                "let '{}' expects {} to {} arguments, got {}",
2704                name, required_count, decl.ports.len(), args.len()
2705            ),
2706        });
2707    }
2708
2709    let mut result = decl.body.clone();
2710    for (i, port) in decl.ports.iter().enumerate() {
2711        let actual = if i < args.len() {
2712            args[i].clone()
2713        } else {
2714            port.default.clone().ok_or_else(|| SvaParseError {
2715                message: format!("missing required argument '{}' for let '{}'", port.name, name),
2716            })?
2717        };
2718        result = substitute_signal(&result, &port.name, &actual);
2719    }
2720    Ok(result)
2721}
2722
2723/// Translate dist items into range constraints for formal verification.
2724/// IEEE 16.14.2: In formal mode, `dist` ≡ `inside` (range restriction only).
2725pub fn translate_dist_to_ranges(items: &[DistItem]) -> Vec<(u64, u64)> {
2726    items.iter().map(|item| {
2727        let max = item.max.unwrap_or(item.min);
2728        (item.min, max)
2729    }).collect()
2730}
2731
2732/// Validate dist list is non-empty.
2733pub fn validate_dist(items: &[DistItem]) -> Result<(), SvaParseError> {
2734    if items.is_empty() {
2735        return Err(SvaParseError { message: "empty dist list".to_string() });
2736    }
2737    Ok(())
2738}
2739
2740/// Resolve a checker instance by binding ports into its assertions.
2741pub fn resolve_checker(
2742    checker: &CheckerDecl,
2743    port_bindings: &[(String, SvaExpr)],
2744) -> Result<Vec<SvaDirective>, SvaParseError> {
2745    checker.assertions.iter().map(|directive| {
2746        let mut resolved_prop = directive.property.clone();
2747        for (port_name, actual) in port_bindings {
2748            resolved_prop = substitute_signal(&resolved_prop, port_name, actual);
2749        }
2750        Ok(SvaDirective {
2751            kind: directive.kind.clone(),
2752            property: resolved_prop,
2753            label: directive.label.clone(),
2754            clock: directive.clock.clone(),
2755            disable_iff: directive.disable_iff.clone(),
2756            action_pass: directive.action_pass.clone(),
2757            action_fail: directive.action_fail.clone(),
2758        })
2759    }).collect()
2760}
2761
2762/// Get checker's random variable quantifier structure.
2763/// Returns (const_rand_vars, per_timestep_rand_vars).
2764pub fn checker_quantifier_structure(checker: &CheckerDecl) -> (Vec<&RandVar>, Vec<&RandVar>) {
2765    let const_vars: Vec<&RandVar> = checker.rand_vars.iter().filter(|v| v.is_const).collect();
2766    let nonconst_vars: Vec<&RandVar> = checker.rand_vars.iter().filter(|v| !v.is_const).collect();
2767    (const_vars, nonconst_vars)
2768}