Skip to main content

logicaffeine_compile/codegen_sva/
sva_to_verify.rs

1//! SVA → Bounded Verification IR Translation
2//!
3//! Translates SvaExpr to a bounded timestep model suitable for Z3 equivalence checking.
4//! Each signal at timestep t becomes a variable "signal@t".
5//! Temporal operators are unrolled to bounded disjunctions/conjunctions.
6
7use super::sva_model::SvaExpr;
8use std::collections::{HashMap, HashSet};
9
10/// A verification expression in the bounded timestep model.
11/// This is a Z3-ready IR — each node maps directly to a Z3 AST construct.
12#[derive(Debug, Clone, PartialEq)]
13pub enum BoundedExpr {
14    /// Boolean variable: "signal@timestep"
15    Var(String),
16    /// Boolean literal
17    Bool(bool),
18    /// Integer literal
19    Int(i64),
20    /// Conjunction
21    And(Box<BoundedExpr>, Box<BoundedExpr>),
22    /// Disjunction
23    Or(Box<BoundedExpr>, Box<BoundedExpr>),
24    /// Negation
25    Not(Box<BoundedExpr>),
26    /// Implication: a → b
27    Implies(Box<BoundedExpr>, Box<BoundedExpr>),
28    /// Equality: a == b
29    Eq(Box<BoundedExpr>, Box<BoundedExpr>),
30    /// Less than: a < b
31    Lt(Box<BoundedExpr>, Box<BoundedExpr>),
32    /// Greater than: a > b
33    Gt(Box<BoundedExpr>, Box<BoundedExpr>),
34    /// Less than or equal: a <= b
35    Lte(Box<BoundedExpr>, Box<BoundedExpr>),
36    /// Greater than or equal: a >= b
37    Gte(Box<BoundedExpr>, Box<BoundedExpr>),
38    /// Unsupported construct (fail closed, not silently true)
39    Unsupported(String),
40
41    // ---- Multi-sorted extensions (SUPERCRUSH S0C) ----
42
43    /// Bitvector constant with explicit width
44    BitVecConst { width: u32, value: u64 },
45    /// Bitvector variable with known width
46    BitVecVar(String, u32),
47    /// Bitvector binary operation
48    BitVecBinary { op: BitVecBoundedOp, left: Box<BoundedExpr>, right: Box<BoundedExpr> },
49    /// Bitvector extraction: operand\[high:low\]
50    BitVecExtract { high: u32, low: u32, operand: Box<BoundedExpr> },
51    /// Bitvector concatenation
52    BitVecConcat(Box<BoundedExpr>, Box<BoundedExpr>),
53    /// Array select: array\[index\]
54    ArraySelect { array: Box<BoundedExpr>, index: Box<BoundedExpr> },
55    /// Array store: array\[index\] := value
56    ArrayStore { array: Box<BoundedExpr>, index: Box<BoundedExpr>, value: Box<BoundedExpr> },
57    /// Integer arithmetic binary operation
58    IntBinary { op: ArithBoundedOp, left: Box<BoundedExpr>, right: Box<BoundedExpr> },
59    /// Comparison returning Bool from Int/BV operands
60    Comparison { op: CmpBoundedOp, left: Box<BoundedExpr>, right: Box<BoundedExpr> },
61    /// Universal quantifier
62    ForAll { var: String, sort: BoundedSort, body: Box<BoundedExpr> },
63    /// Existential quantifier
64    Exists { var: String, sort: BoundedSort, body: Box<BoundedExpr> },
65    /// Uninterpreted function application (system functions like $onehot0, $bits, $clog2)
66    Apply { name: String, args: Vec<BoundedExpr> },
67}
68
69/// Bitvector operations in bounded IR.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum BitVecBoundedOp {
72    And, Or, Xor, Not, Shl, Shr, AShr, Add, Sub, Mul, ULt, SLt, Eq,
73}
74
75/// Arithmetic operations in bounded IR.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ArithBoundedOp {
78    Add, Sub, Mul, Div,
79}
80
81/// Comparison operations in bounded IR.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum CmpBoundedOp {
84    Gt, Lt, Gte, Lte,
85}
86
87/// Sort annotation for bounded quantifiers.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum BoundedSort {
90    Bool,
91    Int,
92    BitVec(u32),
93    /// IEEE 1800-2023: Z3 Real sort for `rand real` checker variables
94    Real,
95}
96
97/// A single match endpoint for a sequence expression.
98///
99/// In IEEE 1800, sequences can have multiple possible match endpoints.
100/// For example, `##[1:3] ack` has three possible match points (at offsets 1, 2, 3).
101/// Each match has a boolean condition (what must hold) and a length (cycles from start).
102///
103/// Used by `translate_sequence()` to compute proper sequence-level AND, OR, intersect,
104/// first_match, throughout, and within semantics.
105#[derive(Debug, Clone)]
106pub struct SequenceMatch {
107    /// The boolean condition that must hold for this match to occur
108    pub condition: BoundedExpr,
109    /// Number of cycles from the sequence start tick to the match endpoint
110    pub length: u32,
111}
112
113/// Result of translating an SVA expression to bounded verification IR.
114pub struct TranslateResult {
115    pub expr: BoundedExpr,
116    pub declarations: Vec<String>, // signal@t variable names
117}
118
119/// Result of translating a directive — includes the semantic role.
120pub struct DirectiveResult {
121    pub expr: BoundedExpr,
122    pub declarations: Vec<String>,
123    pub role: DirectiveRole,
124}
125
126/// Translator that converts SvaExpr to bounded timestep verification IR.
127pub struct SvaTranslator {
128    pub bound: u32,
129    pub(crate) declarations: HashSet<String>,
130    /// Local variable bindings: maps variable name to the BoundedExpr captured
131    /// at assignment time. When a SequenceAction assigns `v = data_in` at timestep t,
132    /// we store `v → translate(data_in, t)`. When LocalVar("v") is later referenced
133    /// at any timestep, we return the captured value, not a fresh `v@t`.
134    local_bindings: HashMap<String, BoundedExpr>,
135    /// Queue timestep: set when entering an implication's consequent to the
136    /// antecedent's evaluation timestep. Used by `const'(expr)` to freeze
137    /// values at the assertion trigger time rather than the consequent's time.
138    queue_timestep: Option<u32>,
139}
140
141impl SvaTranslator {
142    pub fn new(bound: u32) -> Self {
143        Self {
144            bound,
145            declarations: HashSet::new(),
146            local_bindings: HashMap::new(),
147            queue_timestep: None,
148        }
149    }
150
151    /// Translate an SVA expression at a specific timestep.
152    pub fn translate(&mut self, expr: &SvaExpr, t: u32) -> BoundedExpr {
153        match expr {
154            SvaExpr::Signal(name) => {
155                let var_name = format!("{}@{}", name, t);
156                self.declarations.insert(var_name.clone());
157                BoundedExpr::Var(var_name)
158            }
159
160            SvaExpr::Const(value, _width) => BoundedExpr::Int(*value as i64),
161
162            SvaExpr::Rose(inner) => {
163                if t == 0 {
164                    // At t=0, rising edge = signal is high (no prior state)
165                    self.translate(inner, 0)
166                } else {
167                    let current = self.translate(inner, t);
168                    let previous = self.translate(inner, t - 1);
169                    BoundedExpr::And(
170                        Box::new(current),
171                        Box::new(BoundedExpr::Not(Box::new(previous))),
172                    )
173                }
174            }
175
176            SvaExpr::Fell(inner) => {
177                if t == 0 {
178                    BoundedExpr::Not(Box::new(self.translate(inner, 0)))
179                } else {
180                    let current = self.translate(inner, t);
181                    let previous = self.translate(inner, t - 1);
182                    BoundedExpr::And(
183                        Box::new(BoundedExpr::Not(Box::new(current))),
184                        Box::new(previous),
185                    )
186                }
187            }
188
189            SvaExpr::Past(inner, n) => {
190                if t >= *n {
191                    self.translate(inner, t - n)
192                } else {
193                    // No prior state available — return current value (vacuous identity)
194                    // This ensures $stable(sig) ≡ sig == $past(sig,1) at t=0:
195                    // $stable@0 = true, $past(sig,1)@0 = sig@0, so sig@0 == sig@0 = true ✓
196                    self.translate(inner, t)
197                }
198            }
199
200            SvaExpr::And(left, right) => {
201                let l = self.translate(left, t);
202                let r = self.translate(right, t);
203                BoundedExpr::And(Box::new(l), Box::new(r))
204            }
205
206            SvaExpr::Or(left, right) => {
207                let l = self.translate(left, t);
208                let r = self.translate(right, t);
209                BoundedExpr::Or(Box::new(l), Box::new(r))
210            }
211
212            SvaExpr::Not(inner) => {
213                let i = self.translate(inner, t);
214                BoundedExpr::Not(Box::new(i))
215            }
216
217            SvaExpr::Eq(left, right) => {
218                let l = self.translate(left, t);
219                let r = self.translate(right, t);
220                BoundedExpr::Eq(Box::new(l), Box::new(r))
221            }
222
223            SvaExpr::Implication {
224                antecedent,
225                consequent,
226                overlapping,
227            } => {
228                let ante = self.translate(antecedent, t);
229                let cons_t = if *overlapping { t } else { t + 1 };
230                // Set queue_timestep to the antecedent's time for const' freeze
231                let prev_queue = self.queue_timestep;
232                self.queue_timestep = Some(t);
233                let cons = self.translate(consequent, cons_t);
234                self.queue_timestep = prev_queue;
235                BoundedExpr::Implies(Box::new(ante), Box::new(cons))
236            }
237
238            SvaExpr::Delay { body, min, max } => match max {
239                // Unified convention: None = unbounded ($), Some(n) = bounded
240                Some(max_val) if max_val == min => {
241                    // Exact delay ##N: body@{t+N}
242                    self.translate(body, t + min)
243                }
244                Some(max_val) => {
245                    // ##[min:max] body → body@{t+min} ∨ body@{t+min+1} ∨ ... ∨ body@{t+max}
246                    let mut result: Option<BoundedExpr> = None;
247                    for offset in *min..=*max_val {
248                        let step = t + offset;
249                        if step > t + self.bound {
250                            break;
251                        }
252                        let b = self.translate(body, step);
253                        result = Some(match result {
254                            None => b,
255                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(b)),
256                        });
257                    }
258                    result.unwrap_or(BoundedExpr::Bool(false))
259                }
260                None => {
261                    // ##[min:$] body → unbounded, clamp to bound
262                    let mut result: Option<BoundedExpr> = None;
263                    for offset in *min..=self.bound {
264                        let step = t + offset;
265                        if step > t + self.bound {
266                            break;
267                        }
268                        let b = self.translate(body, step);
269                        result = Some(match result {
270                            None => b,
271                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(b)),
272                        });
273                    }
274                    result.unwrap_or(BoundedExpr::Bool(false))
275                }
276            },
277
278            SvaExpr::SEventually(inner) => {
279                // s_eventually(body) → body@{t+1} ∨ body@{t+2} ∨ ... ∨ body@{t+bound}
280                let mut result: Option<BoundedExpr> = None;
281                for offset in 1..=self.bound {
282                    let b = self.translate(inner, t + offset);
283                    result = Some(match result {
284                        None => b,
285                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(b)),
286                    });
287                }
288                result.unwrap_or(BoundedExpr::Bool(false))
289            }
290
291            SvaExpr::Stable(inner) => {
292                // $stable(sig) → sig@t == sig@(t-1)
293                // At t=0, no previous state → vacuously stable
294                if t == 0 {
295                    BoundedExpr::Bool(true)
296                } else {
297                    let current = self.translate(inner, t);
298                    let previous = self.translate(inner, t - 1);
299                    BoundedExpr::Eq(Box::new(current), Box::new(previous))
300                }
301            }
302
303            SvaExpr::Changed(inner) => {
304                // $changed(sig) → !(sig@t == sig@(t-1))
305                // At t=0, no previous state → vacuously not changed
306                if t == 0 {
307                    BoundedExpr::Bool(false)
308                } else {
309                    let current = self.translate(inner, t);
310                    let previous = self.translate(inner, t - 1);
311                    BoundedExpr::Not(Box::new(BoundedExpr::Eq(
312                        Box::new(current),
313                        Box::new(previous),
314                    )))
315                }
316            }
317
318            SvaExpr::Nexttime(inner, n) => {
319                // nexttime[N](body) → body@(t+N)
320                self.translate(inner, t + n)
321            }
322
323            SvaExpr::DisableIff { condition, body } => {
324                // disable iff (cond) body → ¬cond@t → body@t
325                // When disable condition is active, property is vacuously true
326                let cond = self.translate(condition, t);
327                let prop = self.translate(body, t);
328                BoundedExpr::Implies(
329                    Box::new(BoundedExpr::Not(Box::new(cond))),
330                    Box::new(prop),
331                )
332            }
333
334            SvaExpr::Repetition { body, min, max } => {
335                let effective_max = match max {
336                    Some(m) => *m,
337                    None => (*min).max(1) + self.bound,
338                };
339                if *min == effective_max {
340                    // Exact repetition [*N]: body@t ∧ body@{t+1} ∧ ... ∧ body@{t+N-1}
341                    let mut result: Option<BoundedExpr> = None;
342                    for offset in 0..*min {
343                        let b = self.translate(body, t + offset);
344                        result = Some(match result {
345                            None => b,
346                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
347                        });
348                    }
349                    result.unwrap_or(BoundedExpr::Bool(true))
350                } else {
351                    // Range repetition [*min:max]: ∨ over lengths, each a conjunction
352                    let mut outer: Option<BoundedExpr> = None;
353                    for len in *min..=effective_max {
354                        if len > self.bound + t {
355                            break;
356                        }
357                        let mut inner_conj: Option<BoundedExpr> = None;
358                        for offset in 0..len {
359                            let b = self.translate(body, t + offset);
360                            inner_conj = Some(match inner_conj {
361                                None => b,
362                                Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
363                            });
364                        }
365                        let conj = inner_conj.unwrap_or(BoundedExpr::Bool(true));
366                        outer = Some(match outer {
367                            None => conj,
368                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(conj)),
369                        });
370                    }
371                    outer.unwrap_or(BoundedExpr::Bool(false))
372                }
373            }
374
375            SvaExpr::SAlways(inner) => {
376                // s_always(body) at t → body@t ∧ body@{t+1} ∧ ... ∧ body@{bound-1}
377                let remaining = if self.bound > t { self.bound - t } else { 1 };
378                let mut result: Option<BoundedExpr> = None;
379                for offset in 0..remaining {
380                    let b = self.translate(inner, t + offset);
381                    result = Some(match result {
382                        None => b,
383                        Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
384                    });
385                }
386                result.unwrap_or(BoundedExpr::Bool(true))
387            }
388
389            SvaExpr::IfElse { condition, then_expr, else_expr } => {
390                // if (C) P else Q → (C@t → P@t) ∧ (¬C@t → Q@t)
391                let c = self.translate(condition, t);
392                let p = self.translate(then_expr, t);
393                let q = self.translate(else_expr, t);
394                BoundedExpr::And(
395                    Box::new(BoundedExpr::Implies(Box::new(c.clone()), Box::new(p))),
396                    Box::new(BoundedExpr::Implies(
397                        Box::new(BoundedExpr::Not(Box::new(c))),
398                        Box::new(q),
399                    )),
400                )
401            }
402
403            // ── IEEE 1800 Extended (Sprint 1B) ──
404
405            SvaExpr::NotEq(left, right) => {
406                // a != b → ¬(a == b) at timestep t
407                let l = self.translate(left, t);
408                let r = self.translate(right, t);
409                BoundedExpr::Not(Box::new(BoundedExpr::Eq(Box::new(l), Box::new(r))))
410            }
411
412            SvaExpr::LessThan(left, right) => {
413                let l = self.translate(left, t);
414                let r = self.translate(right, t);
415                BoundedExpr::Lt(Box::new(l), Box::new(r))
416            }
417
418            SvaExpr::GreaterThan(left, right) => {
419                let l = self.translate(left, t);
420                let r = self.translate(right, t);
421                BoundedExpr::Gt(Box::new(l), Box::new(r))
422            }
423
424            SvaExpr::LessEqual(left, right) => {
425                let l = self.translate(left, t);
426                let r = self.translate(right, t);
427                BoundedExpr::Lte(Box::new(l), Box::new(r))
428            }
429
430            SvaExpr::GreaterEqual(left, right) => {
431                let l = self.translate(left, t);
432                let r = self.translate(right, t);
433                BoundedExpr::Gte(Box::new(l), Box::new(r))
434            }
435
436            SvaExpr::Ternary { condition, then_expr, else_expr } => {
437                // cond ? a : b → (cond@t ∧ a@t) ∨ (¬cond@t ∧ b@t)
438                let c = self.translate(condition, t);
439                let a = self.translate(then_expr, t);
440                let b = self.translate(else_expr, t);
441                BoundedExpr::Or(
442                    Box::new(BoundedExpr::And(Box::new(c.clone()), Box::new(a))),
443                    Box::new(BoundedExpr::And(
444                        Box::new(BoundedExpr::Not(Box::new(c))),
445                        Box::new(b),
446                    )),
447                )
448            }
449
450            SvaExpr::Throughout { signal, sequence } => {
451                // IEEE 16.9.9: sig throughout seq ≡ (sig[*0:$]) intersect seq
452                // The signal must hold at EVERY cycle of the sequence.
453                // Desugar: get sequence matches, and for each match at length L,
454                // conjoin signal at every tick from t to t+L.
455                let seq_matches = self.translate_sequence(sequence, t);
456                let mut outer: Option<BoundedExpr> = None;
457                for sm in &seq_matches {
458                    // Signal must hold at every tick from t to t+sm.length
459                    let mut sig_conj: Option<BoundedExpr> = None;
460                    for offset in 0..=sm.length {
461                        let s = self.translate(signal, t + offset);
462                        sig_conj = Some(match sig_conj {
463                            None => s,
464                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(s)),
465                        });
466                    }
467                    let sig_all = sig_conj.unwrap_or(BoundedExpr::Bool(true));
468                    // Both signal-at-every-tick AND sequence condition must hold
469                    let case = BoundedExpr::And(
470                        Box::new(sig_all),
471                        Box::new(sm.condition.clone()),
472                    );
473                    outer = Some(match outer {
474                        None => case,
475                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(case)),
476                    });
477                }
478                outer.unwrap_or(BoundedExpr::Bool(false))
479            }
480
481            SvaExpr::Within { inner, outer } => {
482                // IEEE 16.9.10: seq1 within seq2
483                // Inner must start at or after outer start and end at or before outer end.
484                // For each outer match at length L_outer, try each possible inner start
485                // offset s in [0, L_outer], get inner matches, and only keep those where
486                // inner_start + inner_length <= L_outer.
487                let outer_matches = self.translate_sequence(outer, t);
488                let mut result: Option<BoundedExpr> = None;
489                for om in &outer_matches {
490                    // Try starting inner at each offset within the outer's span
491                    for inner_start in 0..=om.length {
492                        let inner_matches = self.translate_sequence(inner, t + inner_start);
493                        for im in &inner_matches {
494                            // Inner must end within outer's span
495                            if inner_start + im.length <= om.length {
496                                let case = BoundedExpr::And(
497                                    Box::new(om.condition.clone()),
498                                    Box::new(im.condition.clone()),
499                                );
500                                result = Some(match result {
501                                    None => case,
502                                    Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(case)),
503                                });
504                            }
505                        }
506                    }
507                }
508                result.unwrap_or(BoundedExpr::Bool(false))
509            }
510
511            SvaExpr::FirstMatch(inner) => {
512                // IEEE 16.9.8: first_match(seq) → only the earliest-completing match.
513                // Get all matches, find the minimum length, and only keep those.
514                // For priority encoding: shortest match condition AND NOT any shorter.
515                let matches = self.translate_sequence(inner, t);
516                if matches.is_empty() {
517                    return BoundedExpr::Bool(false);
518                }
519                let min_length = matches.iter().map(|m| m.length).min().unwrap_or(0);
520                let mut result: Option<BoundedExpr> = None;
521                for m in &matches {
522                    if m.length == min_length {
523                        result = Some(match result {
524                            None => m.condition.clone(),
525                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(m.condition.clone())),
526                        });
527                    }
528                }
529                result.unwrap_or(BoundedExpr::Bool(false))
530            }
531
532            SvaExpr::Intersect { left, right } => {
533                // IEEE 16.9.6: intersect requires both sequences to match AND have
534                // the SAME match length. Only pairs where left.length == right.length
535                // contribute to the result.
536                let left_matches = self.translate_sequence(left, t);
537                let right_matches = self.translate_sequence(right, t);
538                let mut outer: Option<BoundedExpr> = None;
539                for lm in &left_matches {
540                    for rm in &right_matches {
541                        if lm.length == rm.length {
542                            // Same length — this pair is valid
543                            let both = BoundedExpr::And(
544                                Box::new(lm.condition.clone()),
545                                Box::new(rm.condition.clone()),
546                            );
547                            outer = Some(match outer {
548                                None => both,
549                                Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(both)),
550                            });
551                        }
552                    }
553                }
554                // If no common lengths exist, the intersect can NEVER match
555                outer.unwrap_or(BoundedExpr::Bool(false))
556            }
557
558            // ── IEEE 1800 System Functions (Audit) ──
559
560            SvaExpr::OneHot0(inner) => {
561                let sig = self.translate(inner, t);
562                BoundedExpr::Apply {
563                    name: "onehot0".to_string(),
564                    args: vec![sig],
565                }
566            }
567
568            SvaExpr::OneHot(inner) => {
569                let sig = self.translate(inner, t);
570                BoundedExpr::Apply {
571                    name: "onehot".to_string(),
572                    args: vec![sig],
573                }
574            }
575
576            SvaExpr::CountOnes(inner) => {
577                let sig = self.translate(inner, t);
578                BoundedExpr::Apply {
579                    name: "countones".to_string(),
580                    args: vec![sig],
581                }
582            }
583
584            SvaExpr::IsUnknown(_) => {
585                // In 2-state formal verification, X/Z don't exist
586                BoundedExpr::Bool(false)
587            }
588
589            SvaExpr::Sampled(inner) => {
590                // In synchronous single-clock formal, $sampled == identity
591                self.translate(inner, t)
592            }
593
594            SvaExpr::Bits(inner) => {
595                let sig = self.translate(inner, t);
596                BoundedExpr::Apply {
597                    name: "bits".to_string(),
598                    args: vec![sig],
599                }
600            }
601
602            SvaExpr::Clog2(inner) => {
603                let val = self.translate(inner, t);
604                if let BoundedExpr::Int(n) = &val {
605                    let n = *n;
606                    let result = if n <= 1 {
607                        0i64
608                    } else {
609                        // IEEE 1800 $clog2: ceiling of log base 2
610                        // $clog2(n) = ceil(log2(n))
611                        let u = n as u64;
612                        // For power of 2: log2(u) exactly
613                        // For non-power: round up
614                        // u.next_power_of_two() gives the next power of 2 >= u
615                        // trailing_zeros gives log2 of that power
616                        u.next_power_of_two().trailing_zeros() as i64
617                    };
618                    BoundedExpr::Int(result)
619                } else {
620                    BoundedExpr::Apply {
621                        name: "clog2".to_string(),
622                        args: vec![val],
623                    }
624                }
625            }
626
627            // ── Sprint 13 System Functions ──
628
629            SvaExpr::CountBits(inner, control_chars) => {
630                let sig = self.translate(inner, t);
631                BoundedExpr::Apply {
632                    name: format!("countbits_{}", control_chars.iter().collect::<String>()),
633                    args: vec![sig],
634                }
635            }
636
637            SvaExpr::IsUnbounded(_) => {
638                // $isunbounded evaluates to a boolean constant at compile time
639                // In bounded model checking, parameters are always bounded
640                BoundedExpr::Bool(false)
641            }
642
643            // ── Advanced Sequences (Audit) ──
644
645            SvaExpr::GotoRepetition { body, count } => {
646                if *count == 0 {
647                    BoundedExpr::Bool(true)
648                } else if *count > self.bound {
649                    BoundedExpr::Bool(false)
650                } else {
651                    // Disjunction over all C(bound, count) subsets of timestep positions
652                    let combos = combinations(self.bound, *count);
653                    let mut disj: Option<BoundedExpr> = None;
654                    for combo in combos {
655                        let mut conj: Option<BoundedExpr> = None;
656                        for &pos in &combo {
657                            let b = self.translate(body, t + pos);
658                            conj = Some(match conj {
659                                None => b,
660                                Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
661                            });
662                        }
663                        let term = conj.unwrap_or(BoundedExpr::Bool(true));
664                        disj = Some(match disj {
665                            None => term,
666                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(term)),
667                        });
668                    }
669                    disj.unwrap_or(BoundedExpr::Bool(false))
670                }
671            }
672
673            SvaExpr::NonConsecRepetition { body, min, max } => {
674                let effective_max = match max {
675                    Some(m) => *m,
676                    None => self.bound,
677                };
678                if *min > self.bound {
679                    BoundedExpr::Bool(false)
680                } else {
681                    let capped_max = effective_max.min(self.bound);
682                    let all_positions: Vec<u32> = (1..=self.bound).collect();
683                    let mut outer_disj: Option<BoundedExpr> = None;
684                    for count in *min..=capped_max {
685                        let combos = combinations(self.bound, count);
686                        for combo in combos {
687                            // body true at chosen positions, false at all others
688                            let mut conj: Option<BoundedExpr> = None;
689                            for &pos in &all_positions {
690                                let b = self.translate(body, t + pos);
691                                let term = if combo.contains(&pos) {
692                                    b
693                                } else {
694                                    BoundedExpr::Not(Box::new(b))
695                                };
696                                conj = Some(match conj {
697                                    None => term,
698                                    Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(term)),
699                                });
700                            }
701                            let term = conj.unwrap_or(BoundedExpr::Bool(true));
702                            outer_disj = Some(match outer_disj {
703                                None => term,
704                                Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(term)),
705                            });
706                        }
707                    }
708                    outer_disj.unwrap_or(BoundedExpr::Bool(false))
709                }
710            }
711
712            // ── Property Abort Operators (Audit) ──
713
714            SvaExpr::AcceptOn { condition, body } => {
715                // accept_on(C) P ≡ C ∨ P
716                let c = self.translate(condition, t);
717                let p = self.translate(body, t);
718                BoundedExpr::Or(Box::new(c), Box::new(p))
719            }
720
721            SvaExpr::RejectOn { condition, body } => {
722                // reject_on(C) P ≡ ¬C ∧ P
723                let c = self.translate(condition, t);
724                let p = self.translate(body, t);
725                BoundedExpr::And(
726                    Box::new(BoundedExpr::Not(Box::new(c))),
727                    Box::new(p),
728                )
729            }
730
731            // ── Property Connectives (Sprint 1, IEEE 16.12.3-8) ──
732
733            SvaExpr::PropertyNot(inner) => {
734                let i = self.translate(inner, t);
735                BoundedExpr::Not(Box::new(i))
736            }
737
738            SvaExpr::PropertyImplies(left, right) => {
739                let l = self.translate(left, t);
740                let r = self.translate(right, t);
741                BoundedExpr::Implies(Box::new(l), Box::new(r))
742            }
743
744            SvaExpr::PropertyIff(left, right) => {
745                // p iff q → And(Implies(p, q), Implies(q, p))
746                let l = self.translate(left, t);
747                let r = self.translate(right, t);
748                BoundedExpr::And(
749                    Box::new(BoundedExpr::Implies(Box::new(l.clone()), Box::new(r.clone()))),
750                    Box::new(BoundedExpr::Implies(Box::new(r), Box::new(l))),
751                )
752            }
753
754            // ── LTL Temporal Operators (Sprint 2, IEEE 16.12.11-13) ──
755
756            SvaExpr::Always(inner) => {
757                // always p → ∀t ∈ [0, bound). p@t (weak: passes if trace ends)
758                let remaining = if self.bound > t { self.bound - t } else { 1 };
759                let mut result: Option<BoundedExpr> = None;
760                for offset in 0..remaining {
761                    let b = self.translate(inner, t + offset);
762                    result = Some(match result {
763                        None => b,
764                        Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
765                    });
766                }
767                result.unwrap_or(BoundedExpr::Bool(true))
768            }
769
770            SvaExpr::AlwaysBounded { body, min, max } => {
771                // always [m:n] p @ t → ∀i ∈ [m, n]. p@(t+i)
772                let effective_max = match max {
773                    Some(m) => *m,
774                    None => self.bound.saturating_sub(t), // $ clamped to bound
775                };
776                let mut result: Option<BoundedExpr> = None;
777                for i in *min..=effective_max {
778                    if t + i >= self.bound + t { break; } // clamp
779                    let b = self.translate(body, t + i);
780                    result = Some(match result {
781                        None => b,
782                        Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
783                    });
784                }
785                result.unwrap_or(BoundedExpr::Bool(true)) // weak: vacuously true if no ticks
786            }
787
788            SvaExpr::SAlwaysBounded { body, min, max } => {
789                // s_always [m:n] p @ t → ∀i ∈ [m, n]. p@(t+i) (strong: ticks must exist)
790                let mut result: Option<BoundedExpr> = None;
791                for i in *min..=*max {
792                    let b = self.translate(body, t + i);
793                    result = Some(match result {
794                        None => b,
795                        Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
796                    });
797                }
798                result.unwrap_or(BoundedExpr::Bool(true))
799            }
800
801            SvaExpr::EventuallyBounded { body, min, max } => {
802                // eventually [m:n] p @ t → ∃i ∈ [m, n]. p@(t+i)
803                let mut result: Option<BoundedExpr> = None;
804                for i in *min..=*max {
805                    let b = self.translate(body, t + i);
806                    result = Some(match result {
807                        None => b,
808                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(b)),
809                    });
810                }
811                result.unwrap_or(BoundedExpr::Bool(false))
812            }
813
814            SvaExpr::SEventuallyBounded { body, min, max } => {
815                // s_eventually [m:n] p @ t → ∃i ∈ [m, min(n, bound)]. p@(t+i)
816                let effective_max = match max {
817                    Some(m) => *m,
818                    None => self.bound.saturating_sub(t),
819                };
820                let mut result: Option<BoundedExpr> = None;
821                for i in *min..=effective_max {
822                    let b = self.translate(body, t + i);
823                    result = Some(match result {
824                        None => b,
825                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(b)),
826                    });
827                }
828                result.unwrap_or(BoundedExpr::Bool(false))
829            }
830
831            SvaExpr::Until { lhs, rhs, strong, inclusive } => {
832                // Bounded until encoding:
833                // ∃k ∈ [t, t+bound). rhs@k ∧ (∀j ∈ [t, k). lhs@j)
834                //   For inclusive (until_with): ∀j ∈ [t, k]. lhs@j (includes k)
835                //   For strong: rhs MUST appear within bound
836                //   For weak: if no rhs within bound, passes if lhs holds throughout
837                let max_k = t + self.bound;
838                let mut outer: Option<BoundedExpr> = None;
839                for k in t..max_k {
840                    let rhs_at_k = self.translate(rhs, k);
841                    // lhs holds at all j in [t, k) or [t, k] for inclusive
842                    let end = if *inclusive { k + 1 } else { k };
843                    let mut lhs_conj: Option<BoundedExpr> = None;
844                    for j in t..end {
845                        let l = self.translate(lhs, j);
846                        lhs_conj = Some(match lhs_conj {
847                            None => l,
848                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(l)),
849                        });
850                    }
851                    let lhs_all = lhs_conj.unwrap_or(BoundedExpr::Bool(true));
852                    let case = BoundedExpr::And(Box::new(rhs_at_k), Box::new(lhs_all));
853                    outer = Some(match outer {
854                        None => case,
855                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(case)),
856                    });
857                }
858                if !strong {
859                    // Weak: also passes if lhs holds at ALL ticks and rhs never appears
860                    let mut all_lhs: Option<BoundedExpr> = None;
861                    for j in t..max_k {
862                        let l = self.translate(lhs, j);
863                        all_lhs = Some(match all_lhs {
864                            None => l,
865                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(l)),
866                        });
867                    }
868                    let fallback = all_lhs.unwrap_or(BoundedExpr::Bool(true));
869                    outer = Some(match outer {
870                        None => fallback,
871                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(fallback)),
872                    });
873                }
874                outer.unwrap_or(BoundedExpr::Bool(false))
875            }
876
877            // ── Sprint 3: Strong/Weak, Advanced Temporal, Sync Abort ──
878
879            SvaExpr::Strong(inner) => {
880                // strong(seq): existential — at least one match endpoint MUST exist
881                // within bound. Translate as disjunction over sequence match endpoints.
882                // If no match exists → property FAILS.
883                let matches = self.translate_sequence(inner, t);
884                if matches.is_empty() {
885                    return BoundedExpr::Bool(false);
886                }
887                let mut result: Option<BoundedExpr> = None;
888                for m in &matches {
889                    let cond = m.condition.clone();
890                    result = Some(match result {
891                        None => cond,
892                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(cond)),
893                    });
894                }
895                result.unwrap_or(BoundedExpr::Bool(false))
896            }
897
898            SvaExpr::Weak(inner) => {
899                // weak(seq): if no match endpoint exists within bound, property
900                // PASSES (vacuously). Otherwise, at least one match must hold.
901                // Translate as: (exists a match) OR (no match possible within bound).
902                // In bounded model checking the "no match possible" case is when
903                // the trace ends before the sequence can complete — which is
904                // captured by allowing vacuous true when all match conditions are false.
905                let matches = self.translate_sequence(inner, t);
906                if matches.is_empty() {
907                    return BoundedExpr::Bool(true);
908                }
909                // any_match = disjunction of all match conditions
910                let mut any_match: Option<BoundedExpr> = None;
911                for m in &matches {
912                    let cond = m.condition.clone();
913                    any_match = Some(match any_match {
914                        None => cond,
915                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(cond)),
916                    });
917                }
918                // weak: if none of the match conditions hold, pass anyway
919                // This is equivalent to: any_match OR (NOT any_match) = true
920                // BUT that would be trivially true. The correct semantics is:
921                // if the sequence CAN complete (all required ticks exist), then
922                // at least one match must hold. If it CANNOT complete (bound too
923                // small), then pass. We approximate this in bounded BMC by checking
924                // whether the max match length exceeds remaining bound.
925                let max_length = matches.iter().map(|m| m.length).max().unwrap_or(0);
926                if t + max_length > self.bound {
927                    // Sequence cannot complete within bound — weak passes
928                    BoundedExpr::Bool(true)
929                } else {
930                    // Sequence can complete — must have at least one match
931                    any_match.unwrap_or(BoundedExpr::Bool(true))
932                }
933            }
934
935            SvaExpr::SNexttime(inner, n) => {
936                // s_nexttime[N] p → p@(t+N), strong: t+N must exist within bound
937                // IEEE 16.12.10: strong nexttime FAILS if the required timestep
938                // is at or beyond the bound (the tick must exist)
939                if t + n >= self.bound {
940                    BoundedExpr::Bool(false)
941                } else {
942                    self.translate(inner, t + n)
943                }
944            }
945
946            SvaExpr::FollowedBy { antecedent, consequent, overlapping } => {
947                // seq #-# prop ≡ not (seq |-> not prop) (IEEE p.430)
948                // seq #=# prop ≡ not (seq |=> not prop)
949                let impl_expr = SvaExpr::Implication {
950                    antecedent: antecedent.clone(),
951                    consequent: Box::new(SvaExpr::Not(consequent.clone())),
952                    overlapping: *overlapping,
953                };
954                let impl_result = self.translate(&impl_expr, t);
955                BoundedExpr::Not(Box::new(impl_result))
956            }
957
958            SvaExpr::PropertyCase { expression, items, default } => {
959                // case(expr) val: prop; ... → nested if-else
960                let expr_val = self.translate(expression, t);
961                let mut result = match default {
962                    Some(d) => self.translate(d, t),
963                    None => BoundedExpr::Bool(true), // no default → vacuously true
964                };
965                // Build from last to first (nested if-else chain)
966                for (vals, prop) in items.iter().rev() {
967                    let prop_translated = self.translate(prop, t);
968                    // OR of all value matches
969                    let mut cond: Option<BoundedExpr> = None;
970                    for v in vals {
971                        let v_translated = self.translate(v, t);
972                        let eq = BoundedExpr::Eq(Box::new(expr_val.clone()), Box::new(v_translated));
973                        cond = Some(match cond {
974                            None => eq,
975                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(eq)),
976                        });
977                    }
978                    let condition = cond.unwrap_or(BoundedExpr::Bool(false));
979                    // if condition then prop else previous
980                    result = BoundedExpr::And(
981                        Box::new(BoundedExpr::Implies(Box::new(condition.clone()), Box::new(prop_translated))),
982                        Box::new(BoundedExpr::Implies(
983                            Box::new(BoundedExpr::Not(Box::new(condition))),
984                            Box::new(result),
985                        )),
986                    );
987                }
988                result
989            }
990
991            SvaExpr::SyncAcceptOn { condition, body } => {
992                // sync_accept_on(C) P — like accept_on but C sampled at clock ticks only
993                // In single-clock bounded model: same as accept_on
994                let c = self.translate(condition, t);
995                let p = self.translate(body, t);
996                BoundedExpr::Or(Box::new(c), Box::new(p))
997            }
998
999            SvaExpr::SyncRejectOn { condition, body } => {
1000                // sync_reject_on(C) P — like reject_on but C sampled at clock ticks only
1001                let c = self.translate(condition, t);
1002                let p = self.translate(body, t);
1003                BoundedExpr::And(
1004                    Box::new(BoundedExpr::Not(Box::new(c))),
1005                    Box::new(p),
1006                )
1007            }
1008
1009            // ── Sprint 5: Sequence-level AND & OR ──
1010
1011            SvaExpr::SequenceAnd(left, right) => {
1012                // IEEE 16.9.5 Thread semantics: both start at t, both must match,
1013                // composite ends at whichever finishes LAST (max endpoint).
1014                let left_matches = self.translate_sequence(left, t);
1015                let right_matches = self.translate_sequence(right, t);
1016                let mut outer: Option<BoundedExpr> = None;
1017                for lm in &left_matches {
1018                    for rm in &right_matches {
1019                        // Both conditions must hold; composite length = max
1020                        let _composite_length = lm.length.max(rm.length);
1021                        let both = BoundedExpr::And(
1022                            Box::new(lm.condition.clone()),
1023                            Box::new(rm.condition.clone()),
1024                        );
1025                        outer = Some(match outer {
1026                            None => both,
1027                            Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(both)),
1028                        });
1029                    }
1030                }
1031                outer.unwrap_or(BoundedExpr::Bool(false))
1032            }
1033
1034            SvaExpr::SequenceOr(left, right) => {
1035                // IEEE 16.9.7 Union semantics: at least one matches,
1036                // composite match set is the union of both.
1037                let left_matches = self.translate_sequence(left, t);
1038                let right_matches = self.translate_sequence(right, t);
1039                let mut outer: Option<BoundedExpr> = None;
1040                for m in left_matches.iter().chain(right_matches.iter()) {
1041                    outer = Some(match outer {
1042                        None => m.condition.clone(),
1043                        Some(acc) => BoundedExpr::Or(Box::new(acc), Box::new(m.condition.clone())),
1044                    });
1045                }
1046                outer.unwrap_or(BoundedExpr::Bool(false))
1047            }
1048
1049            // ── Sprint 7: Assertion Directives ──
1050
1051            SvaExpr::ImmediateAssert { expression, .. } => {
1052                // Immediate assert → combinational check at each timestep
1053                self.translate(expression, t)
1054            }
1055
1056            // ── Sprint 13: Complex Data Types ──
1057
1058            SvaExpr::FieldAccess { signal, field } => {
1059                let sig = self.translate(signal, t);
1060                // Field access → create a derived variable name
1061                match &sig {
1062                    BoundedExpr::Var(name) => {
1063                        let field_var = format!("{}.{}", name.split('@').next().unwrap_or(name), field);
1064                        let var_name = format!("{}@{}", field_var, t);
1065                        self.declarations.insert(var_name.clone());
1066                        BoundedExpr::Var(var_name)
1067                    }
1068                    _ => sig, // fallback
1069                }
1070            }
1071
1072            SvaExpr::EnumLiteral { value, .. } => {
1073                // Enum literal → integer constant or uninterpreted
1074                BoundedExpr::Var(value.clone())
1075            }
1076
1077            // ── Sprint 14: Endpoint Methods ──
1078
1079            SvaExpr::Triggered(name) => {
1080                let var_name = format!("{}.triggered@{}", name, t);
1081                self.declarations.insert(var_name.clone());
1082                BoundedExpr::Var(var_name)
1083            }
1084
1085            SvaExpr::Matched(name) => {
1086                let var_name = format!("{}.matched@{}", name, t);
1087                self.declarations.insert(var_name.clone());
1088                BoundedExpr::Var(var_name)
1089            }
1090
1091            // ── Sprint 15: Bitwise Operators ──
1092
1093            SvaExpr::BitAnd(l, r) => {
1094                let left = self.translate(l, t);
1095                let right = self.translate(r, t);
1096                BoundedExpr::BitVecBinary { op: BitVecBoundedOp::And, left: Box::new(left), right: Box::new(right) }
1097            }
1098
1099            SvaExpr::BitOr(l, r) => {
1100                let left = self.translate(l, t);
1101                let right = self.translate(r, t);
1102                BoundedExpr::BitVecBinary { op: BitVecBoundedOp::Or, left: Box::new(left), right: Box::new(right) }
1103            }
1104
1105            SvaExpr::BitXor(l, r) => {
1106                let left = self.translate(l, t);
1107                let right = self.translate(r, t);
1108                BoundedExpr::BitVecBinary { op: BitVecBoundedOp::Xor, left: Box::new(left), right: Box::new(right) }
1109            }
1110
1111            SvaExpr::BitNot(inner) => {
1112                let i = self.translate(inner, t);
1113                BoundedExpr::BitVecBinary { op: BitVecBoundedOp::Not, left: Box::new(i.clone()), right: Box::new(i) }
1114            }
1115
1116            SvaExpr::ReductionAnd(inner) => {
1117                let sig = self.translate(inner, t);
1118                BoundedExpr::Apply { name: "reduction_and".to_string(), args: vec![sig] }
1119            }
1120
1121            SvaExpr::ReductionOr(inner) => {
1122                let sig = self.translate(inner, t);
1123                BoundedExpr::Apply { name: "reduction_or".to_string(), args: vec![sig] }
1124            }
1125
1126            SvaExpr::ReductionXor(inner) => {
1127                let sig = self.translate(inner, t);
1128                BoundedExpr::Apply { name: "reduction_xor".to_string(), args: vec![sig] }
1129            }
1130
1131            SvaExpr::BitSelect { signal, index } => {
1132                let s = self.translate(signal, t);
1133                let i = self.translate(index, t);
1134                BoundedExpr::ArraySelect { array: Box::new(s), index: Box::new(i) }
1135            }
1136
1137            SvaExpr::PartSelect { signal, high, low } => {
1138                let s = self.translate(signal, t);
1139                BoundedExpr::BitVecExtract { high: *high, low: *low, operand: Box::new(s) }
1140            }
1141
1142            SvaExpr::Concat(items) => {
1143                if items.len() < 2 {
1144                    return items.first().map(|i| self.translate(i, t)).unwrap_or(BoundedExpr::Bool(false));
1145                }
1146                let mut result = self.translate(&items[0], t);
1147                for item in &items[1..] {
1148                    let r = self.translate(item, t);
1149                    result = BoundedExpr::BitVecConcat(Box::new(result), Box::new(r));
1150                }
1151                result
1152            }
1153
1154            // ── Sprint 10: Local Variables ──
1155
1156            SvaExpr::SequenceAction { expression, assignments } => {
1157                // The expression must hold, and all assignments capture values at this tick.
1158                // IEEE 16.10: assignments bind local variables to values at this timestep.
1159                // v = data_in at tick t → v resolves to data_in@t in subsequent references.
1160                let expr_cond = self.translate(expression, t);
1161                for (name, rhs) in assignments {
1162                    let bound_value = self.translate(rhs, t);
1163                    self.local_bindings.insert(name.clone(), bound_value);
1164                }
1165                expr_cond
1166            }
1167
1168            SvaExpr::LocalVar(name) => {
1169                // IEEE 16.10: Local variable reference resolves to the value captured
1170                // at assignment time, not the current timestep.
1171                if let Some(bound_value) = self.local_bindings.get(name) {
1172                    bound_value.clone()
1173                } else {
1174                    // Fallback: if no binding exists, create a timestep-specific variable
1175                    let var_name = format!("{}@{}", name, t);
1176                    self.declarations.insert(var_name.clone());
1177                    BoundedExpr::Var(var_name)
1178                }
1179            }
1180
1181            // ── Sprint 18: const' cast ──
1182
1183            SvaExpr::ConstCast(inner) => {
1184                // IEEE 16.14.6.1: const'(expr) freezes the value at assertion queue time.
1185                // In an implication, the queue time is the antecedent's timestep.
1186                let freeze_t = self.queue_timestep.unwrap_or(t);
1187                self.translate(inner, freeze_t)
1188            }
1189
1190            // ── Sprint 12: Multi-Clock ──
1191            SvaExpr::Clocked { body, clock, .. } => {
1192                // In multi-clock BMC, each clock gets its own timestep domain.
1193                // For now, tag the variables with the clock domain name.
1194                // This enables downstream multi-clock analysis to distinguish domains.
1195                let inner = self.translate(body, t);
1196                // Wrap with clock domain context — variables in this subtree
1197                // are in the clock domain. We tag this by prefixing declarations.
1198                for decl in self.declarations.clone() {
1199                    if !decl.contains("__clk_") {
1200                        let tagged = format!("__clk_{}__{}", clock, decl);
1201                        self.declarations.insert(tagged);
1202                    }
1203                }
1204                inner
1205            }
1206
1207            // ── Sprint 23: IEEE 1800-2023 ──
1208            SvaExpr::ArrayMap { .. } => {
1209                BoundedExpr::Unsupported("array map with unknown size".to_string())
1210            }
1211            SvaExpr::TypeThis => {
1212                BoundedExpr::Unsupported("type(this) in class scope".to_string())
1213            }
1214            SvaExpr::RealConst(_) => {
1215                // Real constants are used in checker assume constraints (e.g., r > 1.5)
1216                // They participate in Z3 Real arithmetic directly
1217                BoundedExpr::Unsupported("real constant outside checker context".to_string())
1218            }
1219        }
1220    }
1221
1222    /// Translate an SvaExpr as a SEQUENCE, returning all possible match endpoints.
1223    ///
1224    /// Unlike `translate()` which returns a single BoundedExpr, this returns the
1225    /// set of (condition, length) pairs representing all possible matches.
1226    /// This is the foundation for proper IEEE 1800 sequence-level operators:
1227    /// - SequenceAnd: both match, composite at max endpoint
1228    /// - SequenceOr: union of match sets
1229    /// - Intersect: both match at SAME length
1230    /// - first_match: only shortest match
1231    /// - throughout / within: desugared via intersect
1232    pub fn translate_sequence(&mut self, expr: &SvaExpr, t: u32) -> Vec<SequenceMatch> {
1233        match expr {
1234            // --- Delay: ##N or ##[min:max] ---
1235            SvaExpr::Delay { body, min, max } => {
1236                match max {
1237                    // Unified convention: None = unbounded ($), Some(n) = bounded
1238                    Some(max_val) if max_val == min => {
1239                        // Exact delay ##N: single offset at min
1240                        let body_matches = self.translate_sequence(body, t + min);
1241                        body_matches.into_iter().map(|bm| SequenceMatch {
1242                            condition: bm.condition,
1243                            length: min + bm.length,
1244                        }).collect()
1245                    }
1246                    Some(max_val) => {
1247                        // Range delay ##[min:max]: one possible match per offset
1248                        let mut matches = Vec::new();
1249                        for offset in *min..=*max_val {
1250                            if t + offset > t + self.bound { break; }
1251                            let body_matches = self.translate_sequence(body, t + offset);
1252                            for bm in body_matches {
1253                                matches.push(SequenceMatch {
1254                                    condition: bm.condition,
1255                                    length: offset + bm.length,
1256                                });
1257                            }
1258                        }
1259                        if matches.is_empty() {
1260                            matches.push(SequenceMatch {
1261                                condition: BoundedExpr::Bool(false),
1262                                length: *min,
1263                            });
1264                        }
1265                        matches
1266                    }
1267                    None => {
1268                        // Unbounded delay ##[min:$]: clamp to bound
1269                        let mut matches = Vec::new();
1270                        for offset in *min..=self.bound {
1271                            if t + offset > t + self.bound { break; }
1272                            let body_matches = self.translate_sequence(body, t + offset);
1273                            for bm in body_matches {
1274                                matches.push(SequenceMatch {
1275                                    condition: bm.condition,
1276                                    length: offset + bm.length,
1277                                });
1278                            }
1279                        }
1280                        if matches.is_empty() {
1281                            matches.push(SequenceMatch {
1282                                condition: BoundedExpr::Bool(false),
1283                                length: *min,
1284                            });
1285                        }
1286                        matches
1287                    }
1288                }
1289            }
1290
1291            // --- Repetition: [*N] or [*min:max] ---
1292            SvaExpr::Repetition { body, min, max } => {
1293                let effective_max = match max {
1294                    Some(m) => *m,
1295                    None => (*min).max(1) + self.bound,
1296                };
1297                let mut all_matches = Vec::new();
1298                for count in *min..=effective_max {
1299                    if count > self.bound + 1 { break; }
1300                    if count == 0 {
1301                        // [*0] matches immediately (empty sequence)
1302                        all_matches.push(SequenceMatch {
1303                            condition: BoundedExpr::Bool(true),
1304                            length: 0,
1305                        });
1306                        continue;
1307                    }
1308                    // For count N: body must match N consecutive times.
1309                    // The existing translate() evaluates body at offsets 0..N.
1310                    // Length = N - 1 (last match at offset N-1).
1311                    let total_length = count - 1;
1312                    if t + total_length > t + self.bound { break; }
1313                    let mut cond: Option<BoundedExpr> = None;
1314                    for offset in 0..count {
1315                        let b = self.translate(body, t + offset);
1316                        cond = Some(match cond {
1317                            None => b,
1318                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
1319                        });
1320                    }
1321                    all_matches.push(SequenceMatch {
1322                        condition: cond.unwrap_or(BoundedExpr::Bool(true)),
1323                        length: total_length,
1324                    });
1325                }
1326                if all_matches.is_empty() {
1327                    all_matches.push(SequenceMatch {
1328                        condition: BoundedExpr::Bool(false),
1329                        length: 0,
1330                    });
1331                }
1332                all_matches
1333            }
1334
1335            // --- Sequence concatenation via Implication (a ##N b) ---
1336            // In the parser, `a ##N b` → Implication { ante: a, cons: Delay{b,N,None}, overlapping: true }
1337            // In sequence context, this is conjunction, not material implication.
1338            SvaExpr::Implication { antecedent, consequent, overlapping } => {
1339                let ante_matches = self.translate_sequence(antecedent, t);
1340                let mut all_matches = Vec::new();
1341                for am in &ante_matches {
1342                    let gap = if *overlapping { 0u32 } else { 1u32 };
1343                    let cons_start = t + am.length + gap;
1344                    if cons_start > t + self.bound { continue; }
1345                    let cons_matches = self.translate_sequence(consequent, cons_start);
1346                    for cm in &cons_matches {
1347                        let total_length = am.length + gap + cm.length;
1348                        if total_length > self.bound { continue; }
1349                        all_matches.push(SequenceMatch {
1350                            condition: BoundedExpr::And(
1351                                Box::new(am.condition.clone()),
1352                                Box::new(cm.condition.clone()),
1353                            ),
1354                            length: total_length,
1355                        });
1356                    }
1357                }
1358                if all_matches.is_empty() {
1359                    all_matches.push(SequenceMatch {
1360                        condition: BoundedExpr::Bool(false),
1361                        length: 0,
1362                    });
1363                }
1364                all_matches
1365            }
1366
1367            // --- Goto repetition: [->N] ---
1368            SvaExpr::GotoRepetition { body, count } => {
1369                if *count == 0 {
1370                    return vec![SequenceMatch { condition: BoundedExpr::Bool(true), length: 0 }];
1371                }
1372                if *count > self.bound {
1373                    return vec![SequenceMatch { condition: BoundedExpr::Bool(false), length: 0 }];
1374                }
1375                let combos = combinations(self.bound, *count);
1376                let mut matches = Vec::new();
1377                for combo in combos {
1378                    let end_pos = combo.last().copied().unwrap_or(0);
1379                    let mut conj: Option<BoundedExpr> = None;
1380                    for &pos in &combo {
1381                        let b = self.translate(body, t + pos);
1382                        conj = Some(match conj {
1383                            None => b,
1384                            Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(b)),
1385                        });
1386                    }
1387                    matches.push(SequenceMatch {
1388                        condition: conj.unwrap_or(BoundedExpr::Bool(true)),
1389                        length: end_pos,
1390                    });
1391                }
1392                matches
1393            }
1394
1395            // --- Non-consecutive repetition: [=N] ---
1396            SvaExpr::NonConsecRepetition { body, min, max } => {
1397                let effective_max = match max {
1398                    Some(m) => *m,
1399                    None => self.bound,
1400                };
1401                if *min > self.bound {
1402                    return vec![SequenceMatch { condition: BoundedExpr::Bool(false), length: 0 }];
1403                }
1404                let capped_max = effective_max.min(self.bound);
1405                let all_positions: Vec<u32> = (1..=self.bound).collect();
1406                let mut all_matches = Vec::new();
1407                for count in *min..=capped_max {
1408                    let combos = combinations(self.bound, count);
1409                    for combo in combos {
1410                        let mut conj: Option<BoundedExpr> = None;
1411                        for &pos in &all_positions {
1412                            let b = self.translate(body, t + pos);
1413                            let term = if combo.contains(&pos) {
1414                                b
1415                            } else {
1416                                BoundedExpr::Not(Box::new(b))
1417                            };
1418                            conj = Some(match conj {
1419                                None => term,
1420                                Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(term)),
1421                            });
1422                        }
1423                        // Non-consecutive can end anywhere within the bound
1424                        all_matches.push(SequenceMatch {
1425                            condition: conj.unwrap_or(BoundedExpr::Bool(true)),
1426                            length: self.bound,
1427                        });
1428                    }
1429                }
1430                if all_matches.is_empty() {
1431                    all_matches.push(SequenceMatch {
1432                        condition: BoundedExpr::Bool(true),
1433                        length: 0,
1434                    });
1435                }
1436                all_matches
1437            }
1438
1439            // --- Strong/Weak: forward to inner ---
1440            SvaExpr::Strong(inner) | SvaExpr::Weak(inner) => {
1441                self.translate_sequence(inner, t)
1442            }
1443
1444            // --- FirstMatch: only the shortest-length match (IEEE 16.9.8) ---
1445            SvaExpr::FirstMatch(inner) => {
1446                let matches = self.translate_sequence(inner, t);
1447                if matches.is_empty() {
1448                    return vec![SequenceMatch {
1449                        condition: BoundedExpr::Bool(false),
1450                        length: 0,
1451                    }];
1452                }
1453                let min_length = matches.iter().map(|m| m.length).min().unwrap_or(0);
1454                matches.into_iter().filter(|m| m.length == min_length).collect()
1455            }
1456
1457            // --- Intersect: length-matching (IEEE 16.9.6) ---
1458            SvaExpr::Intersect { left, right } => {
1459                let left_matches = self.translate_sequence(left, t);
1460                let right_matches = self.translate_sequence(right, t);
1461                let mut combined = Vec::new();
1462                for lm in &left_matches {
1463                    for rm in &right_matches {
1464                        if lm.length == rm.length {
1465                            combined.push(SequenceMatch {
1466                                condition: BoundedExpr::And(
1467                                    Box::new(lm.condition.clone()),
1468                                    Box::new(rm.condition.clone()),
1469                                ),
1470                                length: lm.length,
1471                            });
1472                        }
1473                    }
1474                }
1475                if combined.is_empty() {
1476                    combined.push(SequenceMatch {
1477                        condition: BoundedExpr::Bool(false),
1478                        length: 0,
1479                    });
1480                }
1481                combined
1482            }
1483
1484            // --- SequenceAnd: thread semantics (IEEE 16.9.5) ---
1485            SvaExpr::SequenceAnd(left, right) => {
1486                let left_matches = self.translate_sequence(left, t);
1487                let right_matches = self.translate_sequence(right, t);
1488                let mut combined = Vec::new();
1489                for lm in &left_matches {
1490                    for rm in &right_matches {
1491                        combined.push(SequenceMatch {
1492                            condition: BoundedExpr::And(
1493                                Box::new(lm.condition.clone()),
1494                                Box::new(rm.condition.clone()),
1495                            ),
1496                            length: lm.length.max(rm.length),
1497                        });
1498                    }
1499                }
1500                if combined.is_empty() {
1501                    combined.push(SequenceMatch {
1502                        condition: BoundedExpr::Bool(false),
1503                        length: 0,
1504                    });
1505                }
1506                combined
1507            }
1508
1509            // --- SequenceOr: union semantics (IEEE 16.9.7) ---
1510            SvaExpr::SequenceOr(left, right) => {
1511                let mut left_matches = self.translate_sequence(left, t);
1512                let right_matches = self.translate_sequence(right, t);
1513                left_matches.extend(right_matches);
1514                if left_matches.is_empty() {
1515                    left_matches.push(SequenceMatch {
1516                        condition: BoundedExpr::Bool(false),
1517                        length: 0,
1518                    });
1519                }
1520                left_matches
1521            }
1522
1523            // --- All other expressions: atomic match at length 0 ---
1524            _ => {
1525                vec![SequenceMatch {
1526                    condition: self.translate(expr, t),
1527                    length: 0,
1528                }]
1529            }
1530        }
1531    }
1532
1533    /// Estimate the timestep span of a sequence expression (how many cycles it covers).
1534    fn sequence_span(&self, expr: &SvaExpr) -> u32 {
1535        match expr {
1536            SvaExpr::Delay { min, max, body } => {
1537                // None = unbounded ($) → use bound as estimate
1538                let delay_span = max.unwrap_or(self.bound);
1539                delay_span + self.sequence_span(body)
1540            }
1541            SvaExpr::Repetition { min, max, body } => {
1542                // None = unbounded ($) → use bound as estimate
1543                let rep_count = max.unwrap_or((*min).max(1) + self.bound);
1544                rep_count * self.sequence_span(body).max(1)
1545            }
1546            SvaExpr::And(l, r) | SvaExpr::Or(l, r) => {
1547                self.sequence_span(l).max(self.sequence_span(r))
1548            }
1549            SvaExpr::Implication { antecedent, consequent, overlapping } => {
1550                let ante_span = self.sequence_span(antecedent);
1551                let cons_span = self.sequence_span(consequent);
1552                ante_span + cons_span + if *overlapping { 0 } else { 1 }
1553            }
1554            SvaExpr::GotoRepetition { count, body } => {
1555                *count * self.sequence_span(body).max(1)
1556            }
1557            SvaExpr::NonConsecRepetition { min, body, .. } => {
1558                *min * self.sequence_span(body).max(1)
1559            }
1560            SvaExpr::AcceptOn { body, .. } | SvaExpr::RejectOn { body, .. } => {
1561                self.sequence_span(body)
1562            }
1563            _ => 1, // atomic signal = 1 cycle
1564        }
1565    }
1566
1567    /// Translate a top-level SVA property: conjoin over all timesteps [0, bound).
1568    /// This models G(property) — the property must hold at every reachable state.
1569    pub fn translate_property(&mut self, expr: &SvaExpr) -> TranslateResult {
1570        let mut result: Option<BoundedExpr> = None;
1571        for t in 0..self.bound {
1572            let step = self.translate(expr, t);
1573            result = Some(match result {
1574                None => step,
1575                Some(acc) => BoundedExpr::And(Box::new(acc), Box::new(step)),
1576            });
1577        }
1578        let expr = result.unwrap_or(BoundedExpr::Bool(true));
1579        let declarations: Vec<String> = self.declarations.iter().cloned().collect();
1580        TranslateResult {
1581            expr,
1582            declarations,
1583        }
1584    }
1585
1586    /// Translate a concurrent assertion directive (IEEE 16.14).
1587    ///
1588    /// Returns a `DirectiveResult` with the translated expression and the directive's
1589    /// semantic role (check, constraint, or reachability query).
1590    pub fn translate_directive(&mut self, directive: &super::sva_model::SvaDirective) -> DirectiveResult {
1591        use super::sva_model::SvaDirectiveKind;
1592
1593        // First apply disable_iff if present
1594        let effective_property = if let Some(ref disable_cond) = directive.disable_iff {
1595            SvaExpr::DisableIff {
1596                condition: Box::new(disable_cond.clone()),
1597                body: Box::new(directive.property.clone()),
1598            }
1599        } else {
1600            directive.property.clone()
1601        };
1602
1603        let translated = self.translate_property(&effective_property);
1604
1605        match directive.kind {
1606            SvaDirectiveKind::Assert => DirectiveResult {
1607                expr: translated.expr,
1608                declarations: translated.declarations,
1609                role: DirectiveRole::Check,
1610            },
1611            SvaDirectiveKind::Assume | SvaDirectiveKind::Restrict => DirectiveResult {
1612                expr: translated.expr,
1613                declarations: translated.declarations,
1614                role: DirectiveRole::Constraint,
1615            },
1616            SvaDirectiveKind::Cover => DirectiveResult {
1617                expr: translated.expr,
1618                declarations: translated.declarations,
1619                role: DirectiveRole::Reachability,
1620            },
1621            SvaDirectiveKind::CoverSequence => DirectiveResult {
1622                expr: translated.expr,
1623                declarations: translated.declarations,
1624                role: DirectiveRole::ReachabilityMultiple,
1625            },
1626        }
1627    }
1628}
1629
1630/// The semantic role of a directive in formal verification.
1631#[derive(Debug, Clone, PartialEq)]
1632pub enum DirectiveRole {
1633    /// Assert: check property holds (negate and check UNSAT)
1634    Check,
1635    /// Assume/Restrict: add as solver constraint
1636    Constraint,
1637    /// Cover property: check reachability (SAT check, not UNSAT)
1638    Reachability,
1639    /// Cover sequence: count ALL matches (multiplicity)
1640    ReachabilityMultiple,
1641}
1642
1643/// Generate all k-element subsets of {1, 2, ..., n}.
1644/// Used for combinatorial expansion of goto/non-consecutive repetitions.
1645fn combinations(n: u32, k: u32) -> Vec<Vec<u32>> {
1646    if k == 0 {
1647        return vec![vec![]];
1648    }
1649    if k > n {
1650        return vec![];
1651    }
1652    let mut result = Vec::new();
1653    fn helper(start: u32, n: u32, k: u32, current: &mut Vec<u32>, result: &mut Vec<Vec<u32>>) {
1654        if current.len() == k as usize {
1655            result.push(current.clone());
1656            return;
1657        }
1658        for i in start..=n {
1659            current.push(i);
1660            helper(i + 1, n, k, current, result);
1661            current.pop();
1662        }
1663    }
1664    helper(1, n, k, &mut Vec::new(), &mut result);
1665    result
1666}
1667
1668/// Count the number of Or-leaves in a BoundedExpr tree.
1669pub fn count_or_leaves(e: &BoundedExpr) -> usize {
1670    match e {
1671        BoundedExpr::Or(left, right) => count_or_leaves(left) + count_or_leaves(right),
1672        _ => 1,
1673    }
1674}
1675
1676/// Count the number of And-leaves in a BoundedExpr tree.
1677pub fn count_and_leaves(e: &BoundedExpr) -> usize {
1678    match e {
1679        BoundedExpr::And(left, right) => count_and_leaves(left) + count_and_leaves(right),
1680        _ => 1,
1681    }
1682}
1683
1684/// Collect all signal names from declarations in a BoundedExpr tree.
1685fn collect_vars_from_bounded(expr: &BoundedExpr, vars: &mut std::collections::HashSet<String>) {
1686    match expr {
1687        BoundedExpr::Var(name) => { vars.insert(name.clone()); }
1688        BoundedExpr::And(l, r) | BoundedExpr::Or(l, r)
1689        | BoundedExpr::Implies(l, r) | BoundedExpr::Eq(l, r)
1690        | BoundedExpr::Lt(l, r) | BoundedExpr::Gt(l, r)
1691        | BoundedExpr::Lte(l, r) | BoundedExpr::Gte(l, r)
1692        | BoundedExpr::BitVecConcat(l, r) => {
1693            collect_vars_from_bounded(l, vars);
1694            collect_vars_from_bounded(r, vars);
1695        }
1696        BoundedExpr::Not(inner) => collect_vars_from_bounded(inner, vars),
1697        BoundedExpr::BitVecVar(name, _) => { vars.insert(name.clone()); }
1698        BoundedExpr::BitVecBinary { left, right, .. }
1699        | BoundedExpr::IntBinary { left, right, .. }
1700        | BoundedExpr::Comparison { left, right, .. } => {
1701            collect_vars_from_bounded(left, vars);
1702            collect_vars_from_bounded(right, vars);
1703        }
1704        BoundedExpr::BitVecExtract { operand, .. } => collect_vars_from_bounded(operand, vars),
1705        BoundedExpr::ArraySelect { array, index } => {
1706            collect_vars_from_bounded(array, vars);
1707            collect_vars_from_bounded(index, vars);
1708        }
1709        BoundedExpr::ArrayStore { array, index, value } => {
1710            collect_vars_from_bounded(array, vars);
1711            collect_vars_from_bounded(index, vars);
1712            collect_vars_from_bounded(value, vars);
1713        }
1714        BoundedExpr::ForAll { body, .. } | BoundedExpr::Exists { body, .. } => {
1715            collect_vars_from_bounded(body, vars);
1716        }
1717        BoundedExpr::Apply { args, .. } => {
1718            for arg in args {
1719                collect_vars_from_bounded(arg, vars);
1720            }
1721        }
1722        BoundedExpr::Bool(_) | BoundedExpr::Int(_)
1723        | BoundedExpr::BitVecConst { .. } | BoundedExpr::Unsupported(_) => {}
1724    }
1725}
1726
1727/// Public wrapper for collect_vars_from_bounded (for testing).
1728pub fn collect_vars_from_bounded_pub(expr: &BoundedExpr, vars: &mut std::collections::HashSet<String>) {
1729    collect_vars_from_bounded(expr, vars);
1730}
1731
1732/// Translate a BoundedExpr (timestep-unrolled) into a VerifyExpr (Z3-ready).
1733///
1734/// This is the bridge from the compile crate to the verify crate.
1735/// Both SVA and FOL translate to BoundedExpr first; this function makes
1736/// them consumable by the Z3 solver for semantic equivalence checking.
1737#[cfg(feature = "verification")]
1738pub fn bounded_to_verify(expr: &BoundedExpr) -> logicaffeine_verify::VerifyExpr {
1739    use logicaffeine_verify::{VerifyExpr, VerifyOp};
1740    match expr {
1741        BoundedExpr::Var(name) => VerifyExpr::Var(name.clone()),
1742        BoundedExpr::Bool(b) => VerifyExpr::Bool(*b),
1743        BoundedExpr::Int(i) => VerifyExpr::Int(*i),
1744        BoundedExpr::And(l, r) => VerifyExpr::binary(
1745            VerifyOp::And,
1746            bounded_to_verify(l),
1747            bounded_to_verify(r),
1748        ),
1749        BoundedExpr::Or(l, r) => VerifyExpr::binary(
1750            VerifyOp::Or,
1751            bounded_to_verify(l),
1752            bounded_to_verify(r),
1753        ),
1754        BoundedExpr::Not(e) => VerifyExpr::not(bounded_to_verify(e)),
1755        BoundedExpr::Implies(l, r) => VerifyExpr::binary(
1756            VerifyOp::Implies,
1757            bounded_to_verify(l),
1758            bounded_to_verify(r),
1759        ),
1760        BoundedExpr::Eq(l, r) => VerifyExpr::binary(
1761            VerifyOp::Eq,
1762            bounded_to_verify(l),
1763            bounded_to_verify(r),
1764        ),
1765        BoundedExpr::Lt(l, r) => VerifyExpr::binary(
1766            VerifyOp::Lt,
1767            bounded_to_verify(l),
1768            bounded_to_verify(r),
1769        ),
1770        BoundedExpr::Gt(l, r) => VerifyExpr::binary(
1771            VerifyOp::Gt,
1772            bounded_to_verify(l),
1773            bounded_to_verify(r),
1774        ),
1775        BoundedExpr::Lte(l, r) => VerifyExpr::binary(
1776            VerifyOp::Lte,
1777            bounded_to_verify(l),
1778            bounded_to_verify(r),
1779        ),
1780        BoundedExpr::Gte(l, r) => VerifyExpr::binary(
1781            VerifyOp::Gte,
1782            bounded_to_verify(l),
1783            bounded_to_verify(r),
1784        ),
1785        BoundedExpr::Unsupported(_) => VerifyExpr::Bool(false),
1786
1787        // ---- Multi-sorted extensions ----
1788        BoundedExpr::BitVecConst { width, value } => VerifyExpr::bv_const(*width, *value),
1789        // Encode the bitvector width into the variable name (`name_bv<width>`), the convention
1790        // the Z3 equivalence encoder uses to recover a var's width. Dropping the width let the
1791        // encoder default to 8 — silently checking the wrong width for non-8-bit obligations,
1792        // and crashing (null AST) when a sized constant forced a width mismatch.
1793        BoundedExpr::BitVecVar(name, width) => VerifyExpr::Var(format!("{name}_bv{width}")),
1794        BoundedExpr::BitVecBinary { op, left, right } => {
1795            use logicaffeine_verify::BitVecOp;
1796            let vop = match op {
1797                BitVecBoundedOp::And => BitVecOp::And,
1798                BitVecBoundedOp::Or => BitVecOp::Or,
1799                BitVecBoundedOp::Xor => BitVecOp::Xor,
1800                BitVecBoundedOp::Not => BitVecOp::Not,
1801                BitVecBoundedOp::Shl => BitVecOp::Shl,
1802                BitVecBoundedOp::Shr => BitVecOp::Shr,
1803                BitVecBoundedOp::AShr => BitVecOp::AShr,
1804                BitVecBoundedOp::Add => BitVecOp::Add,
1805                BitVecBoundedOp::Sub => BitVecOp::Sub,
1806                BitVecBoundedOp::Mul => BitVecOp::Mul,
1807                BitVecBoundedOp::ULt => BitVecOp::ULt,
1808                BitVecBoundedOp::SLt => BitVecOp::SLt,
1809                BitVecBoundedOp::Eq => BitVecOp::Eq,
1810            };
1811            VerifyExpr::bv_binary(vop, bounded_to_verify(left), bounded_to_verify(right))
1812        }
1813        BoundedExpr::BitVecExtract { high, low, operand } => VerifyExpr::BitVecExtract {
1814            high: *high,
1815            low: *low,
1816            operand: Box::new(bounded_to_verify(operand)),
1817        },
1818        BoundedExpr::BitVecConcat(l, r) => VerifyExpr::BitVecConcat(
1819            Box::new(bounded_to_verify(l)),
1820            Box::new(bounded_to_verify(r)),
1821        ),
1822        BoundedExpr::ArraySelect { array, index } => VerifyExpr::Select {
1823            array: Box::new(bounded_to_verify(array)),
1824            index: Box::new(bounded_to_verify(index)),
1825        },
1826        BoundedExpr::ArrayStore { array, index, value } => VerifyExpr::Store {
1827            array: Box::new(bounded_to_verify(array)),
1828            index: Box::new(bounded_to_verify(index)),
1829            value: Box::new(bounded_to_verify(value)),
1830        },
1831        BoundedExpr::IntBinary { op, left, right } => {
1832            let vop = match op {
1833                ArithBoundedOp::Add => VerifyOp::Add,
1834                ArithBoundedOp::Sub => VerifyOp::Sub,
1835                ArithBoundedOp::Mul => VerifyOp::Mul,
1836                ArithBoundedOp::Div => VerifyOp::Div,
1837            };
1838            VerifyExpr::binary(vop, bounded_to_verify(left), bounded_to_verify(right))
1839        }
1840        BoundedExpr::Comparison { op, left, right } => {
1841            let vop = match op {
1842                CmpBoundedOp::Gt => VerifyOp::Gt,
1843                CmpBoundedOp::Lt => VerifyOp::Lt,
1844                CmpBoundedOp::Gte => VerifyOp::Gte,
1845                CmpBoundedOp::Lte => VerifyOp::Lte,
1846            };
1847            VerifyExpr::binary(vop, bounded_to_verify(left), bounded_to_verify(right))
1848        }
1849        BoundedExpr::ForAll { var, sort, body } => {
1850            use logicaffeine_verify::VerifyType;
1851            let ty = match sort {
1852                BoundedSort::Bool => VerifyType::Bool,
1853                BoundedSort::Int => VerifyType::Int,
1854                BoundedSort::BitVec(w) => VerifyType::BitVector(*w),
1855                BoundedSort::Real => VerifyType::Real,
1856            };
1857            VerifyExpr::forall(vec![(var.clone(), ty)], bounded_to_verify(body))
1858        }
1859        BoundedExpr::Exists { var, sort, body } => {
1860            use logicaffeine_verify::VerifyType;
1861            let ty = match sort {
1862                BoundedSort::Bool => VerifyType::Bool,
1863                BoundedSort::Int => VerifyType::Int,
1864                BoundedSort::BitVec(w) => VerifyType::BitVector(*w),
1865                BoundedSort::Real => VerifyType::Real,
1866            };
1867            VerifyExpr::exists(vec![(var.clone(), ty)], bounded_to_verify(body))
1868        }
1869        BoundedExpr::Apply { name, args } => {
1870            VerifyExpr::Apply {
1871                name: name.clone(),
1872                args: args.iter().map(bounded_to_verify).collect(),
1873            }
1874        }
1875    }
1876}
1877
1878/// Extract signal names from a BoundedExpr by collecting all Var names
1879/// and stripping the @timestep suffix.
1880pub fn extract_signal_names(result: &TranslateResult) -> Vec<String> {
1881    let mut signals: HashSet<String> = HashSet::new();
1882    for decl in &result.declarations {
1883        if let Some(at_pos) = decl.find('@') {
1884            signals.insert(decl[..at_pos].to_string());
1885        } else {
1886            signals.insert(decl.clone());
1887        }
1888    }
1889    signals.into_iter().collect()
1890}