Skip to main content

logicaffeine_compile/codegen_sva/
rtl.rs

1//! RTL / transition-system bounded model checking — including multi-bit (bitvector) registers.
2//!
3//! A [`TransitionSystem`] is the classic `(registers, property)` model of synchronous
4//! hardware: each [`Register`] has a width, an initial value, and a next-state expression
5//! over the CURRENT state; a safety property must hold in every reachable state. 1-bit
6//! registers are control/FSM bits; wider registers are datapath, handled through the
7//! bit-blaster. This module time-indexes the model (`r` → `r@t`), lowers it through the
8//! certified prover seam ([`super::sva_to_proof::bounded_to_proof`]), and drives
9//! [`logicaffeine_proof::bmc`]. Pure Rust, certified, Z3-free.
10//!
11//! [`parse_transition_system`] ingests a synthesizable Verilog subset (multi-bit `reg`,
12//! `initial`, one `always @(posedge clk)` block, `assert property`) with a typed (bool /
13//! bitvector) expression parser. Unsupported constructs error out — never a silent
14//! mis-parse.
15
16use super::sva_to_proof::bounded_to_proof;
17use super::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
18use logicaffeine_proof::bmc::{self, BmcOutcome, InductionOutcome};
19use logicaffeine_proof::sat::{find_model, ModelOutcome};
20use logicaffeine_proof::ProofExpr;
21use std::collections::HashMap;
22
23/// A next-state function: a direct expression, or a guarded `if/else` (from RTL reset/mux
24/// logic). Guards reference the current state and free inputs.
25#[derive(Debug, Clone)]
26pub enum NextState {
27    Simple(BoundedExpr),
28    Ite { cond: BoundedExpr, then_: Box<NextState>, else_: Box<NextState> },
29}
30
31/// A hardware register: a width, an optional initial value, and its next-state function.
32#[derive(Debug, Clone)]
33pub struct Register {
34    pub name: String,
35    /// Bit width. `1` is a Boolean control bit; `>1` is a bitvector datapath register.
36    pub width: u32,
37    /// Initial (reset) value, masked to `width`; `None` means a FREE initial state (BMC
38    /// explores all start values — the reset-as-free-input model).
39    pub init: Option<u64>,
40    /// Next-state function over the current (unindexed) state and free inputs.
41    pub next: NextState,
42}
43
44/// A synchronous transition system over Boolean and bitvector registers.
45#[derive(Debug, Clone)]
46pub struct TransitionSystem {
47    pub registers: Vec<Register>,
48    /// The safety property that must hold in every reachable state (unindexed, boolean).
49    pub property: BoundedExpr,
50}
51
52fn mask(width: u32) -> u64 {
53    if width >= 64 {
54        u64::MAX
55    } else {
56        (1u64 << width) - 1
57    }
58}
59
60impl TransitionSystem {
61    /// Bounded model check: is a state violating `property` reachable within `max_k` steps?
62    pub fn bmc(&self, max_k: u32) -> BmcOutcome {
63        if self.lowered_init().is_none()
64            || self.lowered_trans(0).is_none()
65            || self.lowered_property(0).is_none()
66        {
67            return BmcOutcome::Unsupported;
68        }
69        let init = self.lowered_init().unwrap();
70        bmc::find_counterexample(
71            &init,
72            &|t| self.lowered_trans(t).unwrap(),
73            &|t| self.lowered_property(t).unwrap(),
74            max_k,
75        )
76    }
77
78    /// Prove (by k-induction) that `property` holds in EVERY reachable state.
79    pub fn prove_invariant(&self, k: u32) -> InductionOutcome {
80        if self.lowered_init().is_none()
81            || self.lowered_trans(0).is_none()
82            || self.lowered_property(0).is_none()
83        {
84            return InductionOutcome::Unsupported;
85        }
86        let init = self.lowered_init().unwrap();
87        bmc::prove_invariant(
88            &init,
89            &|t| self.lowered_trans(t).unwrap(),
90            &|t| self.lowered_property(t).unwrap(),
91            k,
92        )
93    }
94
95    /// A concrete witnessing execution: a model of `init ∧ trans(0..steps)` with NO property
96    /// negation. For a deterministic controller this is the unique real run from the initial
97    /// state — used to ANIMATE the proven-safe machine, which (by construction) has no
98    /// counterexample to show. Returns the `signal@t` bit assignments of the run, or `None` if
99    /// the obligation leaves the supported fragment.
100    pub fn witness_trace(&self, steps: u32) -> Option<Vec<(String, bool)>> {
101        let mut acc = self.lowered_init()?;
102        for t in 0..steps {
103            let tr = self.lowered_trans(t)?;
104            acc = ProofExpr::And(Box::new(acc), Box::new(tr));
105        }
106        match find_model(&acc) {
107            ModelOutcome::Sat(model) => Some(model),
108            _ => None,
109        }
110    }
111
112    fn lowered_init(&self) -> Option<ProofExpr> {
113        // Only registers with a declared initial value are constrained; the rest are free.
114        let parts: Vec<BoundedExpr> = self
115            .registers
116            .iter()
117            .filter_map(|r| r.init.map(|v| reg_init_eq(&r.name, r.width, v)))
118            .collect();
119        bounded_to_proof(&conj(parts))
120    }
121
122    fn lowered_trans(&self, t: u32) -> Option<ProofExpr> {
123        let parts = self.registers.iter().map(|r| reg_next_eq(r, t)).collect();
124        bounded_to_proof(&conj(parts))
125    }
126
127    fn lowered_property(&self, t: u32) -> Option<ProofExpr> {
128        bounded_to_proof(&time_index(&self.property, t))
129    }
130}
131
132/// `reg@0 = init` as a bounded obligation (boolean equality for 1-bit, `bveq` for wider).
133fn reg_init_eq(name: &str, width: u32, init: u64) -> BoundedExpr {
134    let at0 = format!("{name}@0");
135    if width == 1 {
136        let v = BoundedExpr::Var(at0);
137        if init & 1 == 1 {
138            v
139        } else {
140            BoundedExpr::Not(Box::new(v))
141        }
142    } else {
143        BoundedExpr::BitVecBinary {
144            op: BitVecBoundedOp::Eq,
145            left: Box::new(BoundedExpr::BitVecVar(at0, width)),
146            right: Box::new(BoundedExpr::BitVecConst { width, value: init & mask(width) }),
147        }
148    }
149}
150
151/// `reg@(t+1) = value` as a bounded obligation (boolean equality for 1-bit, `bveq` for wider).
152fn reg_eq_value(name: &str, width: u32, t: u32, rhs: BoundedExpr) -> BoundedExpr {
153    let at_next = format!("{name}@{}", t + 1);
154    if width == 1 {
155        BoundedExpr::Eq(Box::new(BoundedExpr::Var(at_next)), Box::new(rhs))
156    } else {
157        BoundedExpr::BitVecBinary {
158            op: BitVecBoundedOp::Eq,
159            left: Box::new(BoundedExpr::BitVecVar(at_next, width)),
160            right: Box::new(rhs),
161        }
162    }
163}
164
165/// `reg@(t+1) = next(state@t)` as a bounded obligation, expanding guarded updates into
166/// `(cond → reg@(t+1)=then) ∧ (¬cond → reg@(t+1)=else)` (no bitvector-mux node needed).
167fn reg_next_eq(r: &Register, t: u32) -> BoundedExpr {
168    next_constraint(&r.name, r.width, t, &r.next)
169}
170
171fn next_constraint(name: &str, width: u32, t: u32, ns: &NextState) -> BoundedExpr {
172    match ns {
173        NextState::Simple(e) => reg_eq_value(name, width, t, time_index(e, t)),
174        NextState::Ite { cond, then_, else_ } => {
175            let c = time_index(cond, t);
176            BoundedExpr::And(
177                Box::new(BoundedExpr::Implies(
178                    Box::new(c.clone()),
179                    Box::new(next_constraint(name, width, t, then_)),
180                )),
181                Box::new(BoundedExpr::Implies(
182                    Box::new(BoundedExpr::Not(Box::new(c))),
183                    Box::new(next_constraint(name, width, t, else_)),
184                )),
185            )
186        }
187    }
188}
189
190fn conj(mut parts: Vec<BoundedExpr>) -> BoundedExpr {
191    match parts.len() {
192        0 => BoundedExpr::Bool(true),
193        1 => parts.pop().unwrap(),
194        _ => {
195            let mut acc = parts.pop().unwrap();
196            while let Some(p) = parts.pop() {
197                acc = BoundedExpr::And(Box::new(p), Box::new(acc));
198            }
199            acc
200        }
201    }
202}
203
204/// Replace every (unindexed) register reference with its `name@t` form, recursively.
205fn time_index(e: &BoundedExpr, t: u32) -> BoundedExpr {
206    let ri = |x: &BoundedExpr| Box::new(time_index(x, t));
207    match e {
208        BoundedExpr::Var(n) => BoundedExpr::Var(format!("{n}@{t}")),
209        BoundedExpr::BitVecVar(n, w) => BoundedExpr::BitVecVar(format!("{n}@{t}"), *w),
210        BoundedExpr::Bool(_) | BoundedExpr::Int(_) | BoundedExpr::BitVecConst { .. } => e.clone(),
211        BoundedExpr::Not(x) => BoundedExpr::Not(ri(x)),
212        BoundedExpr::And(a, b) => BoundedExpr::And(ri(a), ri(b)),
213        BoundedExpr::Or(a, b) => BoundedExpr::Or(ri(a), ri(b)),
214        BoundedExpr::Implies(a, b) => BoundedExpr::Implies(ri(a), ri(b)),
215        BoundedExpr::Eq(a, b) => BoundedExpr::Eq(ri(a), ri(b)),
216        BoundedExpr::Lt(a, b) => BoundedExpr::Lt(ri(a), ri(b)),
217        BoundedExpr::Gt(a, b) => BoundedExpr::Gt(ri(a), ri(b)),
218        BoundedExpr::Lte(a, b) => BoundedExpr::Lte(ri(a), ri(b)),
219        BoundedExpr::Gte(a, b) => BoundedExpr::Gte(ri(a), ri(b)),
220        BoundedExpr::BitVecBinary { op, left, right } => BoundedExpr::BitVecBinary {
221            op: op.clone(),
222            left: ri(left),
223            right: ri(right),
224        },
225        BoundedExpr::BitVecExtract { high, low, operand } => BoundedExpr::BitVecExtract {
226            high: *high,
227            low: *low,
228            operand: ri(operand),
229        },
230        BoundedExpr::BitVecConcat(a, b) => BoundedExpr::BitVecConcat(ri(a), ri(b)),
231        BoundedExpr::Comparison { op, left, right } => BoundedExpr::Comparison {
232            op: op.clone(),
233            left: ri(left),
234            right: ri(right),
235        },
236        other => other.clone(),
237    }
238}
239
240// ── Verilog → TransitionSystem ──────────────────────────────────────────────────────────
241
242/// A Verilog parse error (kept distinct so the BMC layer can surface it verbatim).
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct RtlParseError {
245    pub message: String,
246}
247
248fn perr<T>(msg: impl Into<String>) -> Result<T, RtlParseError> {
249    Err(RtlParseError { message: msg.into() })
250}
251
252#[derive(Debug, Clone, PartialEq)]
253enum Tok {
254    Ident(String),
255    /// A numeric literal: value plus optional explicit bit width (`4'd5` → (5, Some(4))).
256    Num(u64, Option<u32>),
257    Sym(String),
258}
259
260fn tokenize(src: &str) -> Result<Vec<Tok>, RtlParseError> {
261    let b = src.as_bytes();
262    let mut toks = Vec::new();
263    let mut i = 0;
264    while i < b.len() {
265        let c = b[i] as char;
266        if c.is_whitespace() {
267            i += 1;
268            continue;
269        }
270        if c == '/' && i + 1 < b.len() && b[i + 1] == b'/' {
271            while i < b.len() && b[i] != b'\n' {
272                i += 1;
273            }
274            continue;
275        }
276        if c == '/' && i + 1 < b.len() && b[i + 1] == b'*' {
277            i += 2;
278            while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
279                i += 1;
280            }
281            i += 2;
282            continue;
283        }
284        if c.is_ascii_alphabetic() || c == '_' {
285            let start = i;
286            while i < b.len() && ((b[i] as char).is_ascii_alphanumeric() || b[i] == b'_') {
287                i += 1;
288            }
289            toks.push(Tok::Ident(src[start..i].to_string()));
290            continue;
291        }
292        if c.is_ascii_digit() {
293            let start = i;
294            while i < b.len() && (b[i] as char).is_ascii_digit() {
295                i += 1;
296            }
297            let size_digits = &src[start..i];
298            if i < b.len() && b[i] == b'\'' {
299                // Sized literal `<size>'<base><value>`.
300                i += 1;
301                let base = if i < b.len() {
302                    let ch = b[i] as char;
303                    i += 1;
304                    ch.to_ascii_lowercase()
305                } else {
306                    return perr("malformed sized literal");
307                };
308                let vstart = i;
309                while i < b.len() && ((b[i] as char).is_ascii_alphanumeric() || b[i] == b'_') {
310                    i += 1;
311                }
312                let vstr: String = src[vstart..i].chars().filter(|c| *c != '_').collect();
313                let radix = match base {
314                    'b' => 2,
315                    'o' => 8,
316                    'd' => 10,
317                    'h' => 16,
318                    _ => return perr(format!("unsupported literal base '{base}'")),
319                };
320                let value = u64::from_str_radix(&vstr, radix)
321                    .map_err(|_| RtlParseError { message: format!("bad literal value '{vstr}'") })?;
322                let width: u32 = size_digits
323                    .parse()
324                    .map_err(|_| RtlParseError { message: format!("bad literal size '{size_digits}'") })?;
325                toks.push(Tok::Num(value, Some(width)));
326            } else {
327                let value: u64 = size_digits
328                    .parse()
329                    .map_err(|_| RtlParseError { message: format!("bad number '{size_digits}'") })?;
330                toks.push(Tok::Num(value, None));
331            }
332            continue;
333        }
334        let two = if i + 1 < b.len() { &src[i..i + 2] } else { "" };
335        if matches!(two, "==" | "!=" | "<=" | ">=" | "&&" | "||" | "<<" | ">>") {
336            toks.push(Tok::Sym(two.to_string()));
337            i += 2;
338            continue;
339        }
340        let one = &src[i..i + 1];
341        if "()[]{};:@~&|^=<>!,+-*".contains(one) {
342            toks.push(Tok::Sym(one.to_string()));
343            i += 1;
344            continue;
345        }
346        return perr(format!("unexpected character '{c}'"));
347    }
348    Ok(toks)
349}
350
351/// A typed sub-expression: Boolean, a width-`w` bitvector, or an unsized literal whose width
352/// is determined by context.
353#[derive(Clone)]
354enum Typed {
355    Bool(BoundedExpr),
356    Bv(BoundedExpr, u32),
357    Num(u64),
358}
359
360impl Typed {
361    /// Coerce to a `w`-bit bitvector (or a Boolean when `w == 1`).
362    fn as_width(self, w: u32) -> Result<BoundedExpr, RtlParseError> {
363        match self {
364            Typed::Bv(e, ew) if ew == w => Ok(e),
365            Typed::Bv(_, ew) => perr(format!("width mismatch: expected {w}, got {ew}")),
366            Typed::Num(v) if w == 1 => Ok(BoundedExpr::Bool(v & 1 == 1)),
367            Typed::Num(v) => Ok(BoundedExpr::BitVecConst { width: w, value: v & mask(w) }),
368            Typed::Bool(e) if w == 1 => Ok(e),
369            Typed::Bool(_) => perr(format!("expected a {w}-bit value, got a Boolean")),
370        }
371    }
372    /// Coerce to a Boolean (a wider value is "non-zero").
373    fn as_bool(self) -> Result<BoundedExpr, RtlParseError> {
374        match self {
375            Typed::Bool(e) => Ok(e),
376            Typed::Num(v) => Ok(BoundedExpr::Bool(v != 0)),
377            Typed::Bv(e, w) => Ok(BoundedExpr::Not(Box::new(BoundedExpr::BitVecBinary {
378                op: BitVecBoundedOp::Eq,
379                left: Box::new(e),
380                right: Box::new(BoundedExpr::BitVecConst { width: w, value: 0 }),
381            }))),
382        }
383    }
384    fn width_hint(&self) -> Option<u32> {
385        match self {
386            Typed::Bv(_, w) => Some(*w),
387            Typed::Bool(_) => Some(1),
388            Typed::Num(_) => None,
389        }
390    }
391}
392
393/// Bring two operands to a common bitvector width (at least one must be sized).
394fn bv_pair(a: Typed, b: Typed) -> Result<(BoundedExpr, BoundedExpr, u32), RtlParseError> {
395    let w = a.width_hint().or_else(|| b.width_hint());
396    let w = match w {
397        Some(w) if w >= 1 => w,
398        _ => return perr("ambiguous width: at least one operand must be sized"),
399    };
400    Ok((a.as_width(w)?, b.as_width(w)?, w))
401}
402
403fn bx(e: BoundedExpr) -> Box<BoundedExpr> {
404    Box::new(e)
405}
406
407struct Parser<'a> {
408    t: &'a [Tok],
409    i: usize,
410    widths: HashMap<String, u32>,
411}
412
413impl<'a> Parser<'a> {
414    fn peek(&self) -> Option<&Tok> {
415        self.t.get(self.i)
416    }
417    fn is_sym(&self, s: &str) -> bool {
418        matches!(self.peek(), Some(Tok::Sym(x)) if x == s)
419    }
420    fn is_kw(&self, k: &str) -> bool {
421        matches!(self.peek(), Some(Tok::Ident(x)) if x == k)
422    }
423    fn eat_sym(&mut self, s: &str) -> Result<(), RtlParseError> {
424        if self.is_sym(s) {
425            self.i += 1;
426            Ok(())
427        } else {
428            perr(format!("expected '{s}', found {:?}", self.peek()))
429        }
430    }
431    fn eat_kw(&mut self, k: &str) -> Result<(), RtlParseError> {
432        if self.is_kw(k) {
433            self.i += 1;
434            Ok(())
435        } else {
436            perr(format!("expected '{k}', found {:?}", self.peek()))
437        }
438    }
439    fn ident(&mut self) -> Result<String, RtlParseError> {
440        match self.peek() {
441            Some(Tok::Ident(n)) => {
442                let n = n.clone();
443                self.i += 1;
444                Ok(n)
445            }
446            other => perr(format!("expected identifier, found {other:?}")),
447        }
448    }
449    fn num(&mut self) -> Result<u64, RtlParseError> {
450        match self.peek() {
451            Some(Tok::Num(v, _)) => {
452                let v = *v;
453                self.i += 1;
454                Ok(v)
455            }
456            other => perr(format!("expected a number, found {other:?}")),
457        }
458    }
459
460    // Typed expression grammar (Verilog precedence, low→high):
461    // || , && , | , ^ , & , ==/!= , </<=/>/>= , +/- , * , unary ~/! , primary.
462    fn expr(&mut self) -> Result<Typed, RtlParseError> {
463        self.p_lor()
464    }
465    fn p_lor(&mut self) -> Result<Typed, RtlParseError> {
466        let mut a = self.p_land()?;
467        while self.is_sym("||") {
468            self.i += 1;
469            let b = self.p_land()?;
470            a = Typed::Bool(BoundedExpr::Or(bx(a.as_bool()?), bx(b.as_bool()?)));
471        }
472        Ok(a)
473    }
474    fn p_land(&mut self) -> Result<Typed, RtlParseError> {
475        let mut a = self.p_bor()?;
476        while self.is_sym("&&") {
477            self.i += 1;
478            let b = self.p_bor()?;
479            a = Typed::Bool(BoundedExpr::And(bx(a.as_bool()?), bx(b.as_bool()?)));
480        }
481        Ok(a)
482    }
483    fn p_bor(&mut self) -> Result<Typed, RtlParseError> {
484        let mut a = self.p_bxor()?;
485        while self.is_sym("|") {
486            self.i += 1;
487            let b = self.p_bxor()?;
488            a = self.bitwise(a, b, |x, y| BoundedExpr::Or(bx(x), bx(y)), BitVecBoundedOp::Or)?;
489        }
490        Ok(a)
491    }
492    fn p_bxor(&mut self) -> Result<Typed, RtlParseError> {
493        let mut a = self.p_band()?;
494        while self.is_sym("^") {
495            self.i += 1;
496            let b = self.p_band()?;
497            a = self.bitwise(
498                a,
499                b,
500                |x, y| BoundedExpr::Not(bx(BoundedExpr::Eq(bx(x), bx(y)))),
501                BitVecBoundedOp::Xor,
502            )?;
503        }
504        Ok(a)
505    }
506    fn p_band(&mut self) -> Result<Typed, RtlParseError> {
507        let mut a = self.p_eq()?;
508        while self.is_sym("&") {
509            self.i += 1;
510            let b = self.p_eq()?;
511            a = self.bitwise(a, b, |x, y| BoundedExpr::And(bx(x), bx(y)), BitVecBoundedOp::And)?;
512        }
513        Ok(a)
514    }
515    fn p_eq(&mut self) -> Result<Typed, RtlParseError> {
516        let mut a = self.p_rel()?;
517        while self.is_sym("==") || self.is_sym("!=") {
518            let neq = self.is_sym("!=");
519            self.i += 1;
520            let b = self.p_rel()?;
521            let eq = self.equality(a, b)?;
522            a = Typed::Bool(if neq { BoundedExpr::Not(bx(eq)) } else { eq });
523        }
524        Ok(a)
525    }
526    fn p_rel(&mut self) -> Result<Typed, RtlParseError> {
527        let mut a = self.p_add()?;
528        while self.is_sym("<") || self.is_sym(">") || self.is_sym("<=") || self.is_sym(">=") {
529            let op = match self.peek() {
530                Some(Tok::Sym(s)) => s.clone(),
531                _ => unreachable!(),
532            };
533            self.i += 1;
534            let b = self.p_add()?;
535            let (l, r, _w) = bv_pair(a, b)?;
536            // Unsigned magnitude comparisons via the bit-blaster's ULt.
537            let ult = |x: BoundedExpr, y: BoundedExpr| BoundedExpr::BitVecBinary {
538                op: BitVecBoundedOp::ULt,
539                left: bx(x),
540                right: bx(y),
541            };
542            let res = match op.as_str() {
543                "<" => ult(l, r),
544                ">" => ult(r, l),
545                "<=" => BoundedExpr::Not(bx(ult(r, l))),
546                ">=" => BoundedExpr::Not(bx(ult(l, r))),
547                _ => unreachable!(),
548            };
549            a = Typed::Bool(res);
550        }
551        Ok(a)
552    }
553    fn p_add(&mut self) -> Result<Typed, RtlParseError> {
554        let mut a = self.p_mul()?;
555        while self.is_sym("+") || self.is_sym("-") {
556            let sub = self.is_sym("-");
557            self.i += 1;
558            let b = self.p_mul()?;
559            let (l, r, w) = bv_pair(a, b)?;
560            a = Typed::Bv(
561                BoundedExpr::BitVecBinary {
562                    op: if sub { BitVecBoundedOp::Sub } else { BitVecBoundedOp::Add },
563                    left: bx(l),
564                    right: bx(r),
565                },
566                w,
567            );
568        }
569        Ok(a)
570    }
571    fn p_mul(&mut self) -> Result<Typed, RtlParseError> {
572        let mut a = self.p_unary()?;
573        while self.is_sym("*") {
574            self.i += 1;
575            let b = self.p_unary()?;
576            let (l, r, w) = bv_pair(a, b)?;
577            a = Typed::Bv(
578                BoundedExpr::BitVecBinary { op: BitVecBoundedOp::Mul, left: bx(l), right: bx(r) },
579                w,
580            );
581        }
582        Ok(a)
583    }
584    fn p_unary(&mut self) -> Result<Typed, RtlParseError> {
585        if self.is_sym("!") {
586            self.i += 1;
587            return Ok(Typed::Bool(BoundedExpr::Not(bx(self.p_unary()?.as_bool()?))));
588        }
589        if self.is_sym("~") {
590            self.i += 1;
591            let inner = self.p_unary()?;
592            return Ok(match inner {
593                Typed::Bool(e) => Typed::Bool(BoundedExpr::Not(bx(e))),
594                Typed::Bv(e, w) => Typed::Bv(
595                    BoundedExpr::BitVecBinary {
596                        op: BitVecBoundedOp::Not,
597                        left: bx(e.clone()),
598                        right: bx(e),
599                    },
600                    w,
601                ),
602                Typed::Num(_) => return perr("'~' needs a sized operand"),
603            });
604        }
605        self.p_primary()
606    }
607    fn p_primary(&mut self) -> Result<Typed, RtlParseError> {
608        if self.is_sym("(") {
609            self.i += 1;
610            let e = self.expr()?;
611            self.eat_sym(")")?;
612            return Ok(e);
613        }
614        match self.peek() {
615            Some(Tok::Num(v, w)) => {
616                let (v, w) = (*v, *w);
617                self.i += 1;
618                Ok(match w {
619                    None => Typed::Num(v),
620                    Some(1) => Typed::Bool(BoundedExpr::Bool(v & 1 == 1)),
621                    Some(w) => Typed::Bv(BoundedExpr::BitVecConst { width: w, value: v & mask(w) }, w),
622                })
623            }
624            Some(Tok::Ident(n)) => {
625                let n = n.clone();
626                self.i += 1;
627                match self.widths.get(&n).copied() {
628                    Some(1) | None => Ok(Typed::Bool(BoundedExpr::Var(n))),
629                    Some(w) => Ok(Typed::Bv(BoundedExpr::BitVecVar(n, w), w)),
630                }
631            }
632            other => perr(format!("expected an expression, found {other:?}")),
633        }
634    }
635
636    /// Bitwise `&`/`|`/`^`: Boolean on Boolean operands, bit-blasted on bitvectors.
637    fn bitwise(
638        &self,
639        a: Typed,
640        b: Typed,
641        bool_op: fn(BoundedExpr, BoundedExpr) -> BoundedExpr,
642        bv_op: BitVecBoundedOp,
643    ) -> Result<Typed, RtlParseError> {
644        match (&a, &b) {
645            (Typed::Bv(..), _) | (_, Typed::Bv(..)) => {
646                let (l, r, w) = bv_pair(a, b)?;
647                Ok(Typed::Bv(BoundedExpr::BitVecBinary { op: bv_op, left: bx(l), right: bx(r) }, w))
648            }
649            _ => Ok(Typed::Bool(bool_op(a.as_bool()?, b.as_bool()?))),
650        }
651    }
652
653    /// `==` (the `!=` caller negates): biconditional on Booleans, `bveq` on bitvectors.
654    fn equality(&self, a: Typed, b: Typed) -> Result<BoundedExpr, RtlParseError> {
655        match (&a, &b) {
656            (Typed::Bv(..), _) | (_, Typed::Bv(..)) => {
657                let (l, r, _w) = bv_pair(a, b)?;
658                Ok(BoundedExpr::BitVecBinary { op: BitVecBoundedOp::Eq, left: bx(l), right: bx(r) })
659            }
660            _ => Ok(BoundedExpr::Eq(bx(a.as_bool()?), bx(b.as_bool()?))),
661        }
662    }
663
664    fn skip_to_semi(&mut self) -> Result<(), RtlParseError> {
665        while !self.is_sym(";") {
666            if self.peek().is_none() {
667                return perr("expected ';' before end of input");
668            }
669            self.i += 1;
670        }
671        self.i += 1;
672        Ok(())
673    }
674    fn skip_balanced_parens(&mut self) -> Result<(), RtlParseError> {
675        self.eat_sym("(")?;
676        let mut depth = 1;
677        while depth > 0 {
678            match self.peek() {
679                None => return perr("unbalanced '(' in port list"),
680                Some(Tok::Sym(s)) if s == "(" => depth += 1,
681                Some(Tok::Sym(s)) if s == ")" => depth -= 1,
682                _ => {}
683            }
684            self.i += 1;
685        }
686        Ok(())
687    }
688
689    /// Parse `[hi:lo]` and return the width `hi - lo + 1`.
690    fn parse_range_width(&mut self) -> Result<u32, RtlParseError> {
691        self.eat_sym("[")?;
692        let hi = self.num()? as i64;
693        self.eat_sym(":")?;
694        let lo = self.num()? as i64;
695        self.eat_sym("]")?;
696        if hi < lo {
697            return perr("register range must be [hi:lo] with hi >= lo");
698        }
699        Ok((hi - lo + 1) as u32)
700    }
701
702    /// Parse an `always` body into per-register next-state functions, honoring `if/else`
703    /// (a register unassigned in a branch holds its prior value).
704    fn parse_block(&mut self) -> Result<HashMap<String, NextState>, RtlParseError> {
705        let mut map = HashMap::new();
706        if self.is_kw("begin") {
707            self.i += 1;
708            while !self.is_kw("end") {
709                if self.peek().is_none() {
710                    return perr("missing 'end' in always block");
711                }
712                self.parse_stmt(&mut map)?;
713            }
714            self.i += 1;
715        } else {
716            self.parse_stmt(&mut map)?;
717        }
718        Ok(map)
719    }
720
721    fn parse_stmt(&mut self, map: &mut HashMap<String, NextState>) -> Result<(), RtlParseError> {
722        if self.is_kw("if") {
723            self.i += 1;
724            self.eat_sym("(")?;
725            let cond = self.expr()?.as_bool()?;
726            self.eat_sym(")")?;
727            let then_map = self.parse_block()?;
728            let else_map = if self.is_kw("else") {
729                self.i += 1;
730                self.parse_block()?
731            } else {
732                HashMap::new()
733            };
734            let mut names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
735            names.extend(then_map.keys().cloned());
736            names.extend(else_map.keys().cloned());
737            for n in names {
738                let w = self.widths.get(&n).copied().unwrap_or(1);
739                let hold = if w == 1 {
740                    NextState::Simple(BoundedExpr::Var(n.clone()))
741                } else {
742                    NextState::Simple(BoundedExpr::BitVecVar(n.clone(), w))
743                };
744                let prior = map.get(&n).cloned().unwrap_or(hold);
745                let then_ns = then_map.get(&n).cloned().unwrap_or_else(|| prior.clone());
746                let else_ns = else_map.get(&n).cloned().unwrap_or(prior);
747                map.insert(
748                    n,
749                    NextState::Ite {
750                        cond: cond.clone(),
751                        then_: Box::new(then_ns),
752                        else_: Box::new(else_ns),
753                    },
754                );
755            }
756            Ok(())
757        } else {
758            let n = self.ident()?;
759            let w = self.widths.get(&n).copied().unwrap_or(1);
760            if self.is_sym("<=") {
761                self.i += 1;
762            } else {
763                self.eat_sym("=")?;
764            }
765            let e = self.expr()?.as_width(w)?;
766            self.eat_sym(";")?;
767            map.insert(n, NextState::Simple(e));
768            Ok(())
769        }
770    }
771}
772
773/// Parse a synthesizable Verilog module into a [`TransitionSystem`]. Errors (never silently
774/// mis-parses) on anything outside the supported subset.
775pub fn parse_transition_system(src: &str) -> Result<TransitionSystem, RtlParseError> {
776    let toks = tokenize(src)?;
777    // First pass: collect register widths so the expression parser is correctly typed.
778    // First pass: collect the width of every declared signal (reg/input/output/wire,
779    // wherever declared — including the port list) so the expression parser is correctly
780    // typed. Inputs are free variables; only `reg`s become registers (second pass).
781    let is_decl_kw = |s: &str| matches!(s, "reg" | "input" | "output" | "wire");
782    let mut widths: HashMap<String, u32> = HashMap::new();
783    {
784        let mut j = 0;
785        while j < toks.len() {
786            if matches!(&toks[j], Tok::Ident(k) if is_decl_kw(k)) {
787                j += 1;
788                let mut w = 1u32;
789                if matches!(toks.get(j), Some(Tok::Sym(s)) if s == "[") {
790                    let mut tmp = Parser { t: &toks, i: j, widths: HashMap::new() };
791                    w = tmp.parse_range_width()?;
792                    j = tmp.i;
793                }
794                // Comma-separated names — but stop at the next declaration keyword (port
795                // lists read `input clk, input rst`, not `input clk, rst`).
796                loop {
797                    match toks.get(j) {
798                        Some(Tok::Ident(n)) if !is_decl_kw(n) => {
799                            widths.insert(n.clone(), w);
800                            j += 1;
801                        }
802                        _ => break,
803                    }
804                    if matches!(toks.get(j), Some(Tok::Sym(s)) if s == ",") {
805                        j += 1;
806                        continue;
807                    }
808                    break;
809                }
810            } else {
811                j += 1;
812            }
813        }
814    }
815
816    let mut p = Parser { t: &toks, i: 0, widths };
817    p.eat_kw("module")?;
818    let _name = p.ident()?;
819    if p.is_sym("(") {
820        p.skip_balanced_parens()?;
821    }
822    p.eat_sym(";")?;
823
824    let mut order: Vec<String> = Vec::new();
825    let mut init: HashMap<String, u64> = HashMap::new();
826    let mut next: HashMap<String, NextState> = HashMap::new();
827    let mut property: Option<BoundedExpr> = None;
828
829    loop {
830        match p.peek() {
831            None => return perr("unexpected end of input (missing 'endmodule')"),
832            Some(Tok::Ident(k)) if k == "endmodule" => {
833                p.i += 1;
834                break;
835            }
836            Some(Tok::Ident(k)) if k == "reg" => {
837                p.i += 1;
838                if p.is_sym("[") {
839                    let _ = p.parse_range_width()?;
840                }
841                loop {
842                    let n = p.ident()?;
843                    if !order.contains(&n) {
844                        order.push(n);
845                    }
846                    if p.is_sym(",") {
847                        p.i += 1;
848                        continue;
849                    }
850                    break;
851                }
852                p.eat_sym(";")?;
853            }
854            Some(Tok::Ident(k)) if k == "input" || k == "output" || k == "wire" => {
855                p.skip_to_semi()?;
856            }
857            Some(Tok::Ident(k)) if k == "initial" => {
858                p.i += 1;
859                let mut one = |p: &mut Parser| -> Result<(), RtlParseError> {
860                    let n = p.ident()?;
861                    p.eat_sym("=")?;
862                    let v = p.num()?;
863                    p.eat_sym(";")?;
864                    init.insert(n, v);
865                    Ok(())
866                };
867                if p.is_kw("begin") {
868                    p.i += 1;
869                    while !p.is_kw("end") {
870                        if p.peek().is_none() {
871                            return perr("missing 'end' for initial block");
872                        }
873                        one(&mut p)?;
874                    }
875                    p.i += 1;
876                } else {
877                    one(&mut p)?;
878                }
879            }
880            Some(Tok::Ident(k)) if k == "always" => {
881                p.i += 1;
882                p.eat_sym("@")?;
883                p.eat_sym("(")?;
884                p.eat_kw("posedge")?;
885                let _clk = p.ident()?;
886                p.eat_sym(")")?;
887                let block = p.parse_block()?;
888                for (n, ns) in block {
889                    next.insert(n, ns);
890                }
891            }
892            Some(Tok::Ident(k)) if k == "assert" => {
893                p.i += 1;
894                p.eat_kw("property")?;
895                p.eat_sym("(")?;
896                if p.is_sym("@") {
897                    p.i += 1;
898                    p.eat_sym("(")?;
899                    p.eat_kw("posedge")?;
900                    let _ = p.ident()?;
901                    p.eat_sym(")")?;
902                }
903                let e = p.expr()?.as_bool()?;
904                p.eat_sym(")")?;
905                p.eat_sym(";")?;
906                property = Some(e);
907            }
908            Some(_) => p.skip_to_semi()?,
909        }
910    }
911
912    if order.is_empty() {
913        return perr("no registers found");
914    }
915    let property = match property {
916        Some(p) => p,
917        None => return perr("no 'assert property' found"),
918    };
919
920    let mut registers = Vec::with_capacity(order.len());
921    for name in &order {
922        let width = p.widths.get(name).copied().unwrap_or(1);
923        // `initial` is optional: absent ⇒ a free initial state (reset-as-free-input).
924        let init_val = init.get(name).map(|v| *v & mask(width));
925        let next_ns = next.remove(name).unwrap_or_else(|| {
926            // A register with no update holds its value.
927            if width == 1 {
928                NextState::Simple(BoundedExpr::Var(name.clone()))
929            } else {
930                NextState::Simple(BoundedExpr::BitVecVar(name.clone(), width))
931            }
932        });
933        registers.push(Register { name: name.clone(), width, init: init_val, next: next_ns });
934    }
935
936    Ok(TransitionSystem { registers, property })
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942
943    fn var(n: &str) -> BoundedExpr {
944        BoundedExpr::Var(n.to_string())
945    }
946    fn not(e: BoundedExpr) -> BoundedExpr {
947        BoundedExpr::Not(Box::new(e))
948    }
949    fn reg(name: &str, init: u64, next: BoundedExpr) -> Register {
950        Register {
951            name: name.to_string(),
952            width: 1,
953            init: Some(init),
954            next: NextState::Simple(next),
955        }
956    }
957
958    #[test]
959    fn bmc_finds_toggle_violation() {
960        let ts = TransitionSystem {
961            registers: vec![reg("q", 0, not(var("q")))],
962            property: not(var("q")),
963        };
964        match ts.bmc(5) {
965            BmcOutcome::CounterexampleAt { k, trace } => {
966                assert_eq!(k, 1);
967                assert!(trace.iter().any(|(n, v)| n == "q@1" && *v), "trace: {trace:?}");
968            }
969            other => panic!("expected a counterexample, got {other:?}"),
970        }
971    }
972
973    #[test]
974    fn k_induction_proves_latched_invariant() {
975        let ts = TransitionSystem {
976            registers: vec![reg("x", 1, var("x"))],
977            property: var("x"),
978        };
979        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
980        assert_eq!(ts.bmc(6), BmcOutcome::NoneWithin(6));
981    }
982
983    #[test]
984    fn k_induction_proves_two_register_mutex() {
985        let ts = TransitionSystem {
986            registers: vec![reg("a", 1, var("b")), reg("b", 0, var("a"))],
987            property: not(BoundedExpr::And(Box::new(var("a")), Box::new(var("b")))),
988        };
989        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
990    }
991
992    // ── Real Verilog ingestion ──────────────────────────────────────────────────────────
993
994    #[test]
995    fn verilog_toggle_violation_found() {
996        let src = r#"
997            module toggle(input clk);
998              reg q;
999              initial q = 0;
1000              always @(posedge clk) q <= ~q;
1001              assert property (@(posedge clk) ~q);
1002            endmodule
1003        "#;
1004        let ts = parse_transition_system(src).expect("parses");
1005        match ts.bmc(5) {
1006            BmcOutcome::CounterexampleAt { k, trace } => {
1007                assert_eq!(k, 1);
1008                assert!(trace.iter().any(|(n, v)| n == "q@1" && *v));
1009            }
1010            other => panic!("expected a counterexample, got {other:?}"),
1011        }
1012    }
1013
1014    #[test]
1015    fn verilog_latched_invariant_proven() {
1016        let src = r#"
1017            module latch(input clk);
1018              reg x;
1019              initial x = 1;
1020              always @(posedge clk) x <= x;
1021              assert property (x);
1022            endmodule
1023        "#;
1024        let ts = parse_transition_system(src).expect("parses");
1025        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1026    }
1027
1028    #[test]
1029    fn verilog_mutex_swap_invariant_proven() {
1030        let src = r#"
1031            module mutex(input clk);
1032              reg a;
1033              reg b;
1034              initial begin a = 1; b = 0; end
1035              always @(posedge clk) begin
1036                a <= b;
1037                b <= a;
1038              end
1039              assert property (~(a & b));
1040            endmodule
1041        "#;
1042        let ts = parse_transition_system(src).expect("parses");
1043        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1044    }
1045
1046    // ── Multi-bit (bitvector) datapath registers ────────────────────────────────────────
1047
1048    #[test]
1049    fn verilog_two_bit_counter_reaches_three() {
1050        // A 2-bit counter cnt++ reaches 3 at step 3, violating "cnt != 3".
1051        let src = r#"
1052            module counter(input clk);
1053              reg [1:0] cnt;
1054              initial cnt = 0;
1055              always @(posedge clk) cnt <= cnt + 1;
1056              assert property (cnt != 2'd3);
1057            endmodule
1058        "#;
1059        let ts = parse_transition_system(src).expect("parses");
1060        assert_eq!(ts.registers[0].width, 2);
1061        match ts.bmc(6) {
1062            BmcOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 3),
1063            other => panic!("expected a counterexample, got {other:?}"),
1064        }
1065    }
1066
1067    #[test]
1068    fn verilog_four_bit_counter_bound_is_invariant() {
1069        // A 4-bit counter never exceeds 15 — a genuine (trivial) invariant, PROVEN.
1070        let src = r#"
1071            module counter(input clk);
1072              reg [3:0] cnt;
1073              initial cnt = 0;
1074              always @(posedge clk) cnt <= cnt + 1;
1075              assert property (cnt <= 4'd15);
1076            endmodule
1077        "#;
1078        let ts = parse_transition_system(src).expect("parses");
1079        assert_eq!(ts.registers[0].width, 4);
1080        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1081    }
1082
1083    #[test]
1084    fn verilog_three_bit_counter_violates_a_false_bound() {
1085        // A 3-bit counter DOES reach 5 — "cnt < 5" is violated at step 5.
1086        let src = r#"
1087            module counter(input clk);
1088              reg [2:0] cnt;
1089              initial cnt = 0;
1090              always @(posedge clk) cnt <= cnt + 1;
1091              assert property (cnt < 3'd5);
1092            endmodule
1093        "#;
1094        let ts = parse_transition_system(src).expect("parses");
1095        match ts.bmc(8) {
1096            BmcOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 5),
1097            other => panic!("expected a counterexample, got {other:?}"),
1098        }
1099    }
1100
1101    #[test]
1102    fn verilog_datapath_register_equality_invariant() {
1103        // Two 4-bit regs initialised equal and updated identically stay equal — PROVEN.
1104        let src = r#"
1105            module mirror(input clk);
1106              reg [3:0] x;
1107              reg [3:0] y;
1108              initial begin x = 3; y = 3; end
1109              always @(posedge clk) begin
1110                x <= x + 1;
1111                y <= y + 1;
1112              end
1113              assert property (x == y);
1114            endmodule
1115        "#;
1116        let ts = parse_transition_system(src).expect("parses");
1117        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1118    }
1119
1120    // ── Rejections (no silent mis-parse) ────────────────────────────────────────────────
1121
1122    // ── Reset-as-free-input (the reset INPUT is a free variable each cycle) ──────────────
1123
1124    #[test]
1125    fn verilog_reset_mirrored_registers_stay_equal() {
1126        // Two registers with identical reset+update logic over a FREE reset input stay equal
1127        // — a genuine invariant, PROVEN by k-induction regardless of when reset fires.
1128        let src = r#"
1129            module mirror(input clk, input rst);
1130              reg a;
1131              reg b;
1132              initial begin a = 0; b = 0; end
1133              always @(posedge clk) begin
1134                if (rst) a <= 0; else a <= ~a;
1135                if (rst) b <= 0; else b <= ~b;
1136              end
1137              assert property (a == b);
1138            endmodule
1139        "#;
1140        let ts = parse_transition_system(src).expect("parses reset logic");
1141        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1142    }
1143
1144    #[test]
1145    fn verilog_multibit_reset_clears_counter() {
1146        // A counter that holds unless reset; with reset a free input, "cnt == 0" is NOT an
1147        // invariant (cnt grows when reset stays low), so BMC must find a violation.
1148        let src = r#"
1149            module counter(input clk, input rst);
1150              reg [3:0] cnt;
1151              initial cnt = 0;
1152              always @(posedge clk) if (rst) cnt <= 0; else cnt <= cnt + 1;
1153              assert property (cnt == 4'd0);
1154            endmodule
1155        "#;
1156        let ts = parse_transition_system(src).expect("parses");
1157        match ts.bmc(4) {
1158            BmcOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 1, "rst low for one cycle ⇒ cnt=1"),
1159            other => panic!("expected a counterexample, got {other:?}"),
1160        }
1161    }
1162
1163    #[test]
1164    fn verilog_free_initial_state_is_explored() {
1165        // No `initial`: the register's start value is FREE, so "q is low" is violated
1166        // immediately (q@0 may be high). Proves we explore all initial states soundly.
1167        let src = r#"
1168            module m(input clk);
1169              reg q;
1170              always @(posedge clk) q <= q;
1171              assert property (~q);
1172            endmodule
1173        "#;
1174        let ts = parse_transition_system(src).expect("parses without initial");
1175        assert!(ts.registers[0].init.is_none(), "q must have a free initial state");
1176        match ts.bmc(3) {
1177            BmcOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 0, "a free q can start high"),
1178            other => panic!("expected a step-0 counterexample, got {other:?}"),
1179        }
1180    }
1181
1182    // ── Real hardware: arbiters, flow control, one-hot FSMs ──────────────────────────────
1183
1184    #[test]
1185    fn verilog_arbiter_mutual_exclusion_proven() {
1186        // A 2-master round-robin arbiter. Grants are set in mutually-exclusive branches, so
1187        // "never two grants at once" is a genuine safety invariant — PROVEN by k-induction.
1188        let src = r#"
1189            module arbiter(input clk, input r0, input r1);
1190              reg g0;
1191              reg g1;
1192              reg turn;
1193              initial begin g0 = 0; g1 = 0; turn = 0; end
1194              always @(posedge clk) begin
1195                if (r0 && (!r1 || turn == 0)) begin
1196                  g0 <= 1;
1197                  g1 <= 0;
1198                end else if (r1) begin
1199                  g0 <= 0;
1200                  g1 <= 1;
1201                end else begin
1202                  g0 <= 0;
1203                  g1 <= 0;
1204                end
1205                turn <= ~turn;
1206              end
1207              assert property (~(g0 & g1));
1208            endmodule
1209        "#;
1210        let ts = parse_transition_system(src).expect("parses");
1211        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1212    }
1213
1214    #[test]
1215    fn verilog_naive_arbiter_double_grant_bug_found() {
1216        // The classic arbiter bug: granting each request independently. If both masters
1217        // request at once, BOTH are granted — a mutual-exclusion violation BMC must catch.
1218        let src = r#"
1219            module bad_arbiter(input clk, input r0, input r1);
1220              reg g0;
1221              reg g1;
1222              initial begin g0 = 0; g1 = 0; end
1223              always @(posedge clk) begin
1224                if (r0) g0 <= 1; else g0 <= 0;
1225                if (r1) g1 <= 1; else g1 <= 0;
1226              end
1227              assert property (~(g0 & g1));
1228            endmodule
1229        "#;
1230        let ts = parse_transition_system(src).expect("parses");
1231        match ts.bmc(4) {
1232            BmcOutcome::CounterexampleAt { k, trace } => {
1233                assert_eq!(k, 1, "both requested at t0 ⇒ double grant at t1");
1234                assert!(trace.iter().any(|(n, v)| n == "g0@1" && *v));
1235                assert!(trace.iter().any(|(n, v)| n == "g1@1" && *v));
1236            }
1237            other => panic!("expected a double-grant counterexample, got {other:?}"),
1238        }
1239    }
1240
1241    #[test]
1242    fn verilog_fifo_occupancy_never_overflows_proven() {
1243        // A FIFO occupancy counter (depth 8) over free push/pop inputs: increment only when
1244        // not full, decrement only when not empty. "count never exceeds 8" is a genuine
1245        // datapath invariant — PROVEN for every push/pop sequence, even though `count` is a
1246        // 4-bit register that could otherwise reach 15.
1247        let src = r#"
1248            module fifo(input clk, input push, input pop);
1249              reg [3:0] count;
1250              initial count = 0;
1251              always @(posedge clk)
1252                if (push && (count < 4'd8) && !(pop && (count > 4'd0)))
1253                  count <= count + 1;
1254                else if (pop && (count > 4'd0) && !(push && (count < 4'd8)))
1255                  count <= count - 1;
1256                else
1257                  count <= count;
1258              assert property (count <= 4'd8);
1259            endmodule
1260        "#;
1261        let ts = parse_transition_system(src).expect("parses");
1262        assert_eq!(ts.registers[0].width, 4);
1263        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1264    }
1265
1266    // A COMPLEX signalized intersection: NS/EW through (ns/ew) PLUS protected left turns
1267    // (nsl/ewl) PLUS a pedestrian phase (ped), sequenced by a 9-state controller with a
1268    // per-phase down-counter. Lights: 0=red, 1=green, 2=yellow; ped: 0=don't-walk, 1=walk.
1269    // Each phase fully sets ALL movements to a non-conflicting configuration, which keeps the
1270    // safety property 1-inductive.
1271    const TRAFFIC_SAFE: &str = r#"
1272        module traffic(input clk);
1273          reg [1:0] ns;
1274          reg [1:0] ew;
1275          reg [1:0] nsl;
1276          reg [1:0] ewl;
1277          reg ped;
1278          reg [3:0] phase;
1279          reg [2:0] timer;
1280          initial begin ns=2'd0; ew=2'd0; nsl=2'd1; ewl=2'd0; ped=1'd0; phase=4'd0; timer=3'd1; end
1281          always @(posedge clk)
1282            if (timer == 3'd0) begin
1283              timer <= 3'd1;
1284              if (phase == 4'd0) begin phase<=4'd1; nsl<=2'd2; ns<=2'd0; ew<=2'd0; ewl<=2'd0; ped<=1'd0; end
1285              else if (phase == 4'd1) begin phase<=4'd2; nsl<=2'd0; ns<=2'd1; ew<=2'd0; ewl<=2'd0; ped<=1'd0; end
1286              else if (phase == 4'd2) begin phase<=4'd3; ns<=2'd2; nsl<=2'd0; ew<=2'd0; ewl<=2'd0; ped<=1'd0; end
1287              else if (phase == 4'd3) begin phase<=4'd4; ewl<=2'd1; ns<=2'd0; ew<=2'd0; nsl<=2'd0; ped<=1'd0; end
1288              else if (phase == 4'd4) begin phase<=4'd5; ewl<=2'd2; ns<=2'd0; ew<=2'd0; nsl<=2'd0; ped<=1'd0; end
1289              else if (phase == 4'd5) begin phase<=4'd6; ewl<=2'd0; ew<=2'd1; ns<=2'd0; nsl<=2'd0; ped<=1'd0; end
1290              else if (phase == 4'd6) begin phase<=4'd7; ew<=2'd2; ns<=2'd0; nsl<=2'd0; ewl<=2'd0; ped<=1'd0; end
1291              else if (phase == 4'd7) begin phase<=4'd8; ped<=1'd1; ns<=2'd0; ew<=2'd0; nsl<=2'd0; ewl<=2'd0; end
1292              else begin phase<=4'd0; nsl<=2'd1; ns<=2'd0; ew<=2'd0; ewl<=2'd0; ped<=1'd0; end
1293            end else
1294              timer <= timer - 1;
1295          assert property (~((ns != 2'd0) & (ew != 2'd0)) & ~((ped == 1'd1) & ((ns != 2'd0) | (ew != 2'd0) | (nsl != 2'd0) | (ewl != 2'd0))));
1296        endmodule
1297    "#;
1298
1299    #[test]
1300    fn verilog_complex_traffic_controller_is_proven_safe() {
1301        // No two conflicting movements are ever active together, and pedestrians only get a
1302        // WALK when EVERY vehicle movement is red — PROVEN by k-induction.
1303        let ts = parse_transition_system(TRAFFIC_SAFE).expect("parses");
1304        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1305    }
1306
1307    #[test]
1308    fn verilog_buggy_traffic_controller_crashes() {
1309        // A subtle, dangerous bug: the pedestrian phase raises WALK but leaves NS GREEN, so
1310        // pedestrians are sent into live cross traffic. BMC finds the exact cycle.
1311        let buggy = TRAFFIC_SAFE.replace(
1312            "phase<=4'd8; ped<=1'd1; ns<=2'd0;",
1313            "phase<=4'd8; ped<=1'd1; ns<=2'd1;",
1314        );
1315        assert_ne!(buggy, TRAFFIC_SAFE, "the buggy variant must differ");
1316        let ts = parse_transition_system(&buggy).expect("parses");
1317        match ts.bmc(40) {
1318            BmcOutcome::CounterexampleAt { .. } => {}
1319            other => panic!("expected a pedestrian-vs-traffic conflict, got {other:?}"),
1320        }
1321    }
1322
1323    #[test]
1324    fn witness_trace_of_safe_controller_is_a_conflict_free_run() {
1325        // The proven-safe controller has no counterexample to animate, so the Studio shows a
1326        // WITNESS execution instead. That witness must be a real run from the declared initial
1327        // state in which the safety property holds at EVERY step.
1328        let ts = parse_transition_system(TRAFFIC_SAFE).expect("parses");
1329        let steps = 16u32;
1330        let model = ts.witness_trace(steps).expect("a witness run exists");
1331        let m: HashMap<String, bool> = model.into_iter().collect();
1332        let bv = |name: &str, t: u32, width: u32| -> u64 {
1333            (0..width)
1334                .filter(|i| *m.get(&format!("{name}@{t}#{i}")).unwrap_or(&false))
1335                .fold(0u64, |acc, i| acc | (1 << i))
1336        };
1337        let bit = |name: &str, t: u32| -> bool { *m.get(&format!("{name}@{t}")).unwrap_or(&false) };
1338        // The run starts in the declared initial state: NS-left green, everything else red.
1339        assert_eq!(bv("nsl", 0, 2), 1, "init: NS-left should be green");
1340        assert_eq!(bv("ns", 0, 2), 0, "init: NS-through should be red");
1341        for t in 0..steps {
1342            let (ns, ew, nsl, ewl) = (bv("ns", t, 2), bv("ew", t, 2), bv("nsl", t, 2), bv("ewl", t, 2));
1343            let ped = bit("ped", t);
1344            let cross = ns != 0 && ew != 0;
1345            let ped_conflict = ped && (ns != 0 || ew != 0 || nsl != 0 || ewl != 0);
1346            assert!(
1347                !cross && !ped_conflict,
1348                "witness conflicts at step {t}: ns={ns} ew={ew} nsl={nsl} ewl={ewl} ped={ped}"
1349            );
1350        }
1351    }
1352
1353    #[test]
1354    fn verilog_onehot_fsm_stays_one_hot_proven() {
1355        // A 3-state one-hot ring FSM. Exactly one of a/b/c is high — PROVEN invariant: the
1356        // rotation a<=c, b<=a, c<=b permutes the bits, preserving one-hotness.
1357        let src = r#"
1358            module onehot(input clk);
1359              reg a;
1360              reg b;
1361              reg c;
1362              initial begin a = 1; b = 0; c = 0; end
1363              always @(posedge clk) begin
1364                a <= c;
1365                b <= a;
1366                c <= b;
1367              end
1368              assert property ((a | b | c) & ~(a & b) & ~(a & c) & ~(b & c));
1369            endmodule
1370        "#;
1371        let ts = parse_transition_system(src).expect("parses");
1372        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven);
1373    }
1374}