Tasks / trusted base

LeanModels.Sv.Sem2

ls-sv semantics

Lines
1098
file
LeanModels/Sv/Sem2.lean
sha256
9b421e3718b1e8b1449aaa4d04b4b9ef8dad8ca3c62ae6991bc7e04f12f6e738
statements that rely on it
_example, add, aes_sbox, arbiter, clmul, fdivp, matmul, mul, sorter
download
LeanModels.Sv.Sem2.lean
LeanModels/Sv/Sem2.leanLeanModels.Sv.Sem2.lean
import LeanModels.Sv.Semantics

/-!
# SV semantic tier (`LeanModels.Sv` — phase 2, "Design2")

The CV32E40P semantic-tier extension of the M0 cycle semantics: bit/element
selects (read and write, constant and dynamic index), constant part-selects,
replication, reduction operators, bitwise width conversion, signed
comparisons (`$signed` casts are same-width bit identities and vanish at
instantiation), `case` / `unique case` / `case … inside` (§12.5.4), and
**asynchronous active-low reset** event lists (`@(posedge clk or negedge
rst_n)` — the `[areset rn]` discipline every clocked CV32E40P module uses).

The M0 layer (`Ast.lean`/`Semantics.lean`) is **untouched**: this file
defines a superset AST (`Expr2`/`Lhs2`/`Stmt2`/`Process2`/`Design2`) and its
own interpreter, reusing the M0 value core (`Basic.lean`), state, schedule
oracle, `Res`, and the M0 operator evaluators. Instantiated sv-0.2 envelopes
lower into `Design2` via `PDesign.instantiate2` (`Param2.lean`).

## The differential ledger (rows first, rules second)

Every rule below was pinned on Xcelium 24.03 **and** Icarus Verilog (both
agreeing) before it was implemented; the `#guard` rows at the end of this
file replay the exact pinned facts (`scratchpad/rows/pin1–pin4.sv`, plus the
probe examples under `Examples/system-verilog/sem2/` in the phase-2 harness
`harness/sv/diff_test2.py`). `case … inside` is the one exception: Icarus
does not parse the construct at all, so its rows are Xcelium-verified only
(recorded gap).

* **Select read** (`v[i]`, `m[i]` on multi-dim packed): a select of `elemW`
  bits at offset `i*elemW`. An `x`/`z` bit in the index, or an out-of-range
  index, reads all-x (pin1 R02/R03/R05, pin2a W05). Multi-dim packed arrays
  are ONE bit vector; `m[i]` is the `elemW`-bit chunk (R04).
* **Select write**: an `x`/`z`-bit or out-of-range index writes NOTHING —
  the target holds (pin2a W01/W03/W06/W08, pin4 Z05). A known in-range index
  writes exactly the addressed bits (W02/W04).
* **Reductions**: `|`,`&`,`^` fold the bit tables of `Logic.or/and/xor`
  (so `|` with any 1 is 1, `&` with any 0 is 0, else x when any x/z);
  `~|`,`~&`,`~^`/`^~` are their negations (pin1 R08–R11, pin4 Z07).
* **Width conversion** (`Resize`): **bitwise**, never the arithmetic
  x-collapse — zero-extension keeps x bits, truncation keeps the low bits
  (pin4 Z01–Z04).
* **Signed comparison** (both operands signed, LRM §11.8.1): two's-
  complement compare; any x/z bit gives x (pin4 S01–S05). `$signed`/
  `$unsigned` at equal width changes no bits — the *operator* carries the
  signedness, so the casts vanish at instantiation.
* **Plain `case`** matches with **case equality** (`===`, exact 4-state:
  subject `xx` matches item `xx`, `x ≠ z`), first match wins, else
  `default`, else no-op (pin2a C01/C02).
* **`case … inside`** matches with wildcard equality `==?` (§12.5.4,
  §11.4.6): `x`/`z` bits in the ITEM are don't-cares; a subject `x`/`z` bit
  in a cared position is a non-match (falls through, `default` if nothing
  matches). Pin2b I01–I09 (Xcelium).
* **`unique`/`unique0`/`priority`** retain their check mode as data and select
  the first matching arm when the claim holds (pin2a C04/C05, pin2b I08/I09).
  The public `run2` path validates the selected settled state and returns the
  named loud result `CaseCheck:unique`, `CaseCheck:unique0`, or
  `CaseCheck:priority` on a violation; it never silently falls through to
  plain-case behavior. The low-level statement evaluator remains the
  structure-preserving first-match primitive used by the validator.
* **Async active-low reset** (`Process2.alwaysFFR`): the LRM two-edge
  sensitivity at cycle granularity. `cycleStep2` inserts a **reset-event
  phase** between the input comb-settle and the posedge phase: for each
  `alwaysFFR` process whose reset signal made a **negedge** transition
  (§9.4.2: `1→0`, `1→x/z`, `x/z→0`) between the end of the previous cycle
  and this cycle's settled inputs, the WHOLE body runs pre-edge (NBA
  discipline, then commit, then settle). Running the whole body — not just
  a reset branch — is load-bearing: on `rst_n: 1→x` the canonical body
  `if (!rst_n) … else …` takes the ELSE branch at the negedge event, so a
  downstream FF samples the *new* value at the same posedge (pin3 CYCLE 6,
  both simulators). Reset-assert pre-edge visibility is pin3 CYCLE 3/0: a
  same-cycle dependent FF samples the already-reset value. While reset
  stays asserted no new event fires, but the posedge still runs the body,
  whose reset branch holds the state (CYCLE 4). Reset DEassertion (`0→1`,
  `x→1`) is a posedge of `rst_n` and fires nothing (CYCLE 5/7).
  Level-vs-edge honesty: the phase is **edge-triggered** on the cycle
  boundary values (previous cycle's final value vs this cycle's settled
  value); sub-cycle reset pulses are not representable at cycle
  granularity — that is the granularity of the whole M0 scheduler, not a
  new approximation.
-/

namespace LeanModels.Sv

/-! ## Value-core additions (`LVec`) -/

namespace LVec

/-- Read `w` bits starting at bit `lo` (LSB-first). Positions beyond the
source width read `x` — the LRM's out-of-range select value. In-range
constant slices (`v[msb:lsb]` after instantiation) never hit the x-fill. -/
def extract (v : LVec) (lo w : Nat) : LVec :=
  ⟨.ofFn (n := w) fun i => v.bits[lo + i.val]?.getD .lx⟩

/-- Write `part` into `v` at bit offset `lo` (LSB-first); bits of `part`
that would land beyond `v`'s width are dropped (cannot arise from typed
sources). The select-write primitive. -/
def insertAt (v : LVec) (lo : Nat) (part : LVec) : LVec :=
  ⟨.ofFn (n := v.width) fun i =>
    if lo ≤ i.val ∧ i.val < lo + part.width then part.bits[i.val - lo]?.getD .lx
    else v.bits[i.val]?.getD .lx⟩

/-- Two's-complement value if fully known, else `none` (signed compare
support; width 0 gives 0). -/
def toInt? (v : LVec) : Option Int :=
  match v.toNat? with
  | none => none
  | some n =>
      if v.width == 0 then some 0
      else if n < 2 ^ (v.width - 1) then some (n : Int)
      else some ((n : Int) - (2 : Int) ^ v.width)

/-- Bitwise width conversion (implicit conversion of an unsigned operand,
LRM §10.7/§11.8.2): zero-extend or keep the low bits. **Not** the
arithmetic x-collapse — x/z bits pass through bit by bit (pin4 Z01–Z04). -/
def resizeU (v : LVec) (w : Nat) : LVec :=
  ⟨.ofFn (n := w) fun i => v.bits[i.val]?.getD .l0⟩

/-- Replication `{n{v}}` (§11.4.12.1). -/
def replVec (n : Nat) (v : LVec) : LVec :=
  ⟨(Array.replicate n v.bits).flatten⟩

/-- Wildcard-equality case match (`==?` as a case-inside guard, §12.5.4 /
§11.4.6, restricted to same-width operands): item bits `x`/`z` are
don't-cares; every cared position must hold a *known, equal* subject bit.
A subject x/z bit in a cared position is a fall-through, exactly like a
definite mismatch (the `x` result of `==?` does not select a case arm). -/
def matchesWild (subj item : LVec) : Bool :=
  subj.eqWildcard item == .l1

end LVec

/-! ## The semantic-tier AST -/

/-- `case` statement check qualifier (§12.5.3). Kept as data so the
 structure-preserving statement evaluator can select arms without a rewrite;
 the public `run2` validator turns a violated claim into a named loud result. -/
inductive CaseCheck where
  | none | unique | unique0 | priority
deriving Repr, BEq, DecidableEq, Inhabited

/-- Matching mode for an integral case statement.  The constructor is shared
by the symbolic envelope and the instantiated semantic tier; qualifiers stay
in `CaseCheck` and do not alter execution. -/
inductive CaseMode where
  | normal | inside | casez | casex
deriving Repr, BEq, DecidableEq, Inhabited

def CaseMode.isNormal : CaseMode → Bool
  | .normal => true
  | _ => false

def CaseMode.isInside : CaseMode → Bool
  | .inside => true
  | _ => false

/-- Semantic-tier expressions: the M0 vocabulary plus selects, replication,
reductions, bitwise resize, and signed comparison. `scmp` ops are the four
order comparisons (`.lt/.le/.gt/.ge` of `BinOp`) with two's-complement
semantics — the extractor marks a comparison signed only when BOTH operands
are signed (§11.8.1); `$signed` casts at equal width change no bits and
vanish at instantiation. -/
inductive Expr2 where
  | resizeSigned (width : Nat) (operand : Expr2)
  /-- Convert x/z bits to zero at a 2-state storage boundary (§6.3.1). -/
  | squash2 (operand : Expr2)
  | lit (value : LVec)
  | ident (name : String)
  | unary (op : UnaryOp) (arg : Expr2)
  | binary (op : BinOp) (left right : Expr2)
  /-- Signed order comparison (`op` must be one of `.lt .le .gt .ge`). -/
  | scmp (op : BinOp) (left right : Expr2)
  /-- Logical `&&` / `||` (`isAnd` selects), §11.4.7: 1-bit result;
  definite-false (`&&`) / definite-true (`||`) operands decide even when
  the other side is x (pin5 L01–L12). -/
  | logical (isAnd : Bool) (left right : Expr2)
  | ternary (cond thenE elseE : Expr2)
  | concat (parts : Array Expr2)
  /-- Constant slice: `w` bits at LSB offset `lo` (an instantiated
  `v[msb:lsb]` or constant-index element select). -/
  | slice (base : Expr2) (lo w : Nat)
  /-- Dynamic element select: `elemW` bits at offset `index*elemW`
  (`elemW = 1` is a plain bit select). x/z or out-of-range index → all-x. -/
  | index (base : Expr2) (idx : Expr2) (elemW : Nat)
  | repl (count : Nat) (arg : Expr2)
  | reduce (op : RedOp) (arg : Expr2)
  /-- Bitwise width conversion of an unsigned operand (zero-extend /
  truncate). Signed widening resolves at instantiation or is loud. -/
  | resize (width : Nat) (arg : Expr2)
  | unsupported (svKind : String) (text : String)
deriving Repr, BEq, Inhabited

/-- Assignment targets: whole signal, constant slice, dynamic element select,
or a concatenation of direct leaves. Concatenations are used by continuous
assignments and are updated from one sampled RHS value. -/
inductive Lhs2 where
  | ident (name : String)
  | slice (name : String) (lo w : Nat)
  | index (name : String) (idx : Expr2) (elemW : Nat)
  | concat (parts : List Lhs2)
deriving Repr, BEq, Inhabited

/-- The written signal's name. -/
def Lhs2.name : Lhs2 → String
  | .ident n | .slice n _ _ | .index n _ _ => n
  | .concat (part :: _) => part.name
  | .concat [] => ""

mutual
/-- Semantic-tier statements: M0 shapes over `Expr2`/`Lhs2` plus `case`.
`items` are `CaseItem2`s in source order (a comma list is ONE item with
several patterns); `inside` selects wildcard matching (§12.5.4) over case
equality. -/
inductive Stmt2 where
  | blockingAssign (target : Lhs2) (value : Expr2)
  | nbaAssign (target : Lhs2) (value : Expr2)
  | ifStmt (cond : Expr2) (thenBranch : Stmt2) (elseBranch : Option Stmt2)
  | block (body : Array Stmt2)
  | caseStmt (subject : Expr2) (items : Array CaseItem2)
      (default? : Option Stmt2) (mode : CaseMode) (check : CaseCheck)
  | unsupported (svKind : String) (text : String)
deriving Repr, Inhabited

/-- One case arm: pattern list (source order) and body. -/
inductive CaseItem2 where
  | mk (pats : Array Expr2) (body : Stmt2)
deriving Repr, Inhabited
end

def CaseItem2.pats : CaseItem2 → Array Expr2 | .mk p _ => p
def CaseItem2.body : CaseItem2 → Stmt2 | .mk _ b => b

/-- Semantic-tier processes. `alwaysFFR` is
`always_ff @(posedge clock or negedge rstn)` — the async active-low reset
shape (see the module docstring's reset-event ledger). Continuous `assign`
targets may be selects (partial drivers: several assigns drive disjoint
slices of one signal, ff_one-style). -/
inductive Process2 where
  | alwaysFF (clock : String) (body : Stmt2)
  | alwaysPlain (clock : String) (body : Stmt2)
  | alwaysComb (body : Stmt2)
  | assign (target : Lhs2) (value : Expr2)
  | alwaysFFR (clock rstn : String) (body : Stmt2) (resetHigh : Bool := false)
  | unsupported (svKind : String) (text : String)
deriving Repr, Inhabited

/-- A semantic-tier design (same `Decl` record as M0 — multi-dim packed
declarations are ONE vector; element widths live in the select nodes). -/
structure Design2 where
  name : String
  decls : Array Decl
  processes : Array Process2
deriving Repr, Inhabited

def Design2.inputNames (d : Design2) : Array String :=
  d.decls.filterMap fun dc => if dc.isInput then some dc.name else none

def Design2.outputNames (d : Design2) : Array String :=
  d.decls.filterMap fun dc => if dc.isOutput then some dc.name else none

/-- Loud-node scan (harness early warning; the interpreter is loud
regardless, exactly like M0). -/
def Design2.hasUnsupported (d : Design2) : Bool :=
  d.processes.any fun p =>
    match p with
    | .unsupported _ _ => true
    | .alwaysFF _ s | .alwaysPlain _ s | .alwaysComb s | .alwaysFFR _ _ s _ => stmtHas s
    | .assign t e => lhsHas t || exprHas e
where
  exprHas : Expr2 → Bool
    | .unsupported _ _ => true
    | .lit _ | .ident _ => false
    | .unary _ a | .repl _ a | .reduce _ a | .resize _ a
    | .resizeSigned _ a | .squash2 a => exprHas a
    | .binary _ a b | .scmp _ a b | .logical _ a b => exprHas a || exprHas b
    | .ternary c a b => exprHas c || exprHas a || exprHas b
    | .slice a _ _ => exprHas a
    | .index a i _ => exprHas a || exprHas i
    | .concat ps => ps.attach.any fun ⟨p, _⟩ => exprHas p
  lhsHas : Lhs2 → Bool
    | .ident _ | .slice .. => false
    | .index _ i _ => exprHas i
    | .concat _ => false
  stmtHas : Stmt2 → Bool
    | .unsupported _ _ => true
    | .blockingAssign t e | .nbaAssign t e => lhsHas t || exprHas e
    | .ifStmt c t e =>
        exprHas c || stmtHas t || (match e with | some s => stmtHas s | none => false)
    | .block body => body.attach.any fun ⟨s, _⟩ => stmtHas s
    | .caseStmt subj items dflt _ _ =>
        exprHas subj
        || items.attach.any (fun ⟨it, _⟩ => caseItemHas it)
        || (match dflt with | some s => stmtHas s | none => false)
  caseItemHas : CaseItem2 → Bool
    | .mk pats body =>
        pats.attach.any (fun ⟨p, _⟩ => exprHas p) || stmtHas body

/-! ## Case matching -/

/-- One case guard: plain `case` uses case equality (`===`, exact 4-state
match — pin2a C01); `case … inside` uses wildcard equality (`==?` with
item-side don't-cares — pin2b). Either way a non-1 result falls through. -/
def caseHit : CaseMode → LVec → LVec → Bool
  | .normal, subj, item => subj == item
  | .inside, subj, item => LVec.matchesWild subj item
  | .casez, subj, item => LVec.matchesCase false subj item
  | .casex, subj, item => LVec.matchesCase true subj item

/-! ## The NBA queue

Semantic-tier NBA entries carry the resolved write window: `(name, lo,
part)`. An assign whose dynamic index is x/z/out-of-range enqueues nothing
(the pinned no-op rule) — resolution happens at ISSUE time, like the RHS
evaluation (§10.4.2). -/

abbrev NbaQueue2 := List (String × Nat × LVec)

/-- Commit in issue order; last write to overlapping bits wins. -/
def commitNba2 (st : SvState) (nba : NbaQueue2) : SvState :=
  nba.foldl
    (fun s (u : String × Nat × LVec) =>
      match SvState.lookup s u.1 with
      | some old => SvState.set s u.1 (old.insertAt u.2.1 u.2.2)
      | none => s)
    st

/-! ## The fueled interpreter core -/

mutual

/-- Evaluate a semantic-tier expression (M0 discipline: fuel matched
first, decremented into every recursive call; both ternary arms evaluated;
loud on `unsupported` nodes reached). -/
def evalExpr2 (fuel : Nat) (st : SvState) (e : Expr2) : Res LVec :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
    match e with
    | .lit v => .ok v
    | .ident name => readSignal st name
    | .unary op a => do
        return evalUnaryOp op (← evalExpr2 fuel st a)
    | .resizeSigned width operand => do
        return (← evalExpr2 fuel st operand).resizeSigned width
    | .squash2 operand => do
        return (← evalExpr2 fuel st operand).toTwoState
    | .binary op l r => do
        let a ← evalExpr2 fuel st l
        let b ← evalExpr2 fuel st r
        return evalBinOp op a b
    | .scmp op l r => do
        let a ← evalExpr2 fuel st l
        let b ← evalExpr2 fuel st r
        match a.toInt?, b.toInt? with
        | some x, some y =>
            return .ofLogic (match op with
              | .lt => if x < y then .l1 else .l0
              | .le => if x ≤ y then .l1 else .l0
              | .gt => if x > y then .l1 else .l0
              | .ge => if x ≥ y then .l1 else .l0
              | _ => .lx)  -- non-order op: outside the constructor contract
        | _, _ => return .ofLogic .lx
    | .logical isAnd l r => do
        let a ← evalExpr2 fuel st l
        let b ← evalExpr2 fuel st r
        let res : Logic :=
          if isAnd then
            if a.isKnownZero || b.isKnownZero then .l0
            else if a.condTrue && b.condTrue then .l1
            else .lx
          else
            if a.condTrue || b.condTrue then .l1
            else if a.isKnownZero && b.isKnownZero then .l0
            else .lx
        return .ofLogic res
    | .ternary c t f => do
        let cv ← evalExpr2 fuel st c
        let tv ← evalExpr2 fuel st t
        let fv ← evalExpr2 fuel st f
        return LVec.ternary cv tv fv
    | .concat parts => do
        let vs ← evalExprs2 fuel st parts.toList
        return LVec.concatMany vs.toArray
    | .slice base lo w => do
        return (← evalExpr2 fuel st base).extract lo w
    | .index base idx elemW => do
        let bv ← evalExpr2 fuel st base
        let iv ← evalExpr2 fuel st idx
        match iv.toNat? with
        | some i =>
            if (i + 1) * elemW ≤ bv.width then
              return bv.extract (i * elemW) elemW
            else
              return LVec.xVec elemW      -- out-of-range read: all-x (W05)
        | none => return LVec.xVec elemW  -- x/z index read: all-x (R02/R03/R05)
    | .repl n a => do
        return LVec.replVec n (← evalExpr2 fuel st a)
    | .reduce op a => do
        return .ofLogic (evalRedOp op (← evalExpr2 fuel st a))
    | .resize w a => do
        return (← evalExpr2 fuel st a).resizeU w
    | .unsupported svKind _ => .unsupported s!"unsupported expression '{svKind}'"

def evalExprs2 (fuel : Nat) (st : SvState) (es : List Expr2) : Res (List LVec) :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
    match es with
    | [] => .ok []
    | e :: rest => do
        let v ← evalExpr2 fuel st e
        let vs ← evalExprs2 fuel st rest
        return v :: vs

/-- Resolve an assignment target to its write window `(name, lo, width)` —
`none` window = the pinned no-write case (x/z or out-of-range dynamic
index; the statement is still executed for its RHS side… M0 expressions are
pure, so "executed" just means the assignment is dropped). -/
def resolveLhs2 (fuel : Nat) (st : SvState) : Lhs2 → Res (String × Option (Nat × Nat))
  | .ident n => do
      match SvState.lookup st n with
      | some old => return (n, some (0, old.width))
      | none => .unsupported s!"unknown assignment target '{n}'"
  | .slice n lo w => .ok (n, some (lo, w))
  | .index n idx elemW => do
      let iv ← evalExpr2 fuel st idx
      match SvState.lookup st n with
      | none => .unsupported s!"unknown assignment target '{n}'"
      | some old =>
          match iv.toNat? with
          | some i =>
              if (i + 1) * elemW ≤ old.width then
                return (n, some (i * elemW, elemW))
              else
                return (n, none)  -- OOB write: no-op (Z05)
          | none => return (n, none)  -- x/z-index write: no-op (W01/W03/W06/W08)
  | .concat _ => .unsupported "concatenated target requires continuous-assignment execution"

/-- Resolve every leaf of a continuous concatenation. The extractor only
emits concatenations whose leaves are direct, statically sized targets, but
the resolver still checks every leaf and rejects a dynamic no-write window:
continuous concatenation needs one sampled RHS and a definite destination
width for each slice. -/
def resolveConcatLhs2 (fuel : Nat) (st : SvState) : Lhs2 →
    Res (List (String × Nat × Nat))
  | .concat parts =>
      parts.foldlM (init := ([] : List (String × Nat × Nat)))
        (fun acc part => do
          let windows ← resolveConcatLhs2 fuel st part
          return acc ++ windows)
  | target => do
      let (name, window) ← resolveLhs2 fuel st target
      match window with
      | some (lo, width) => return [(name, lo, width)]
      | none => .unsupported "continuous concatenation target has no fixed window"

/-- Apply a sampled continuous-assignment RHS to concatenated leaves. LVec is
LSB-first, while a concatenation lists its leaves MSB-first, so the first
leaf receives the high slice and the remaining width decreases toward zero. -/
def writeConcatLhs2 (st : SvState) (value : LVec)
    (windows : List (String × Nat × Nat)) : Res SvState :=
  let total := windows.foldl (fun n (_, _, width) => n + width) 0
  let replacement := value.resizeU total
  let rec go (state : SvState) (remaining : List (String × Nat × Nat))
      (remainingWidth : Nat) : Res SvState :=
    match remaining with
    | [] => .ok state
    | (name, lo, width) :: rest =>
        match SvState.lookup state name with
        | none => .unsupported s!"unknown assign target '{name}'"
        | some old =>
            let nextWidth := remainingWidth - width
            let part := replacement.extract nextWidth width
            go (SvState.set state name (old.insertAt lo part)) rest nextWidth
  go st windows total

/-- Execute one statement (blocking hits the state via `insertAt`;
nonblocking enqueues the resolved window; `case` per `caseHit`, first
match, `default`, else no-op — check qualifiers do not alter execution). -/
def execStmt2 (fuel : Nat) (st : SvState) (nba : NbaQueue2) (stmt : Stmt2) :
    Res (SvState × NbaQueue2) :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
    match stmt with
    | .blockingAssign target value => do
        let v ← evalExpr2 fuel st value
        match target with
        | .concat _ =>
            let windows ← resolveConcatLhs2 fuel st target
            let st' ← writeConcatLhs2 st v windows
            return (st', nba)
        | _ =>
            let (n, win) ← resolveLhs2 fuel st target
            match win with
            | none => return (st, nba)
            | some (lo, w) =>
                match SvState.lookup st n with
                | some old => return (SvState.set st n (old.insertAt lo (v.resizeU w)), nba)
                | none => .unsupported s!"unknown assignment target '{n}'"
    | .nbaAssign target value => do
        let v ← evalExpr2 fuel st value
        let (n, win) ← resolveLhs2 fuel st target
        match win with
        | none => return (st, nba)
        | some (lo, w) => return (st, nba ++ [(n, lo, v.resizeU w)])
    | .ifStmt cond thenBranch elseBranch => do
        let c ← evalExpr2 fuel st cond
        if c.condTrue then
          execStmt2 fuel st nba thenBranch
        else
          match elseBranch with
          | some s => execStmt2 fuel st nba s
          | none => .ok (st, nba)
    | .caseStmt subject items default? mode _check => do
        let sv ← evalExpr2 fuel st subject
        execCase2 fuel st nba sv items.toList default? mode
    | .block body => execStmts2 fuel st nba body.toList
    | .unsupported svKind _ => .unsupported s!"unsupported statement '{svKind}'"

/-- Walk the case items in source order; first hit executes; exhausted
items fall to `default` (no default: no-op — pin2a C05 / pin2b I09). -/
def execCase2 (fuel : Nat) (st : SvState) (nba : NbaQueue2) (subj : LVec) :
    List CaseItem2 → Option Stmt2 → CaseMode → Res (SvState × NbaQueue2)
  | [], default?, _ =>
      match fuel with
      | 0 => .timeout
      | fuel + 1 =>
          match default? with
          | some s => execStmt2 fuel st nba s
          | none => .ok (st, nba)
  | .mk pats body :: rest, default?, mode =>
      match fuel with
      | 0 => .timeout
      | fuel + 1 => do
          let pvs ← evalExprs2 fuel st pats.toList
          if pvs.any (fun pv => caseHit mode subj pv) then
            execStmt2 fuel st nba body
          else
            execCase2 fuel st nba subj rest default? mode


def execStmts2 (fuel : Nat) (st : SvState) (nba : NbaQueue2) (ss : List Stmt2) :
    Res (SvState × NbaQueue2) :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
    match ss with
    | [] => .ok (st, nba)
    | s :: rest => do
        let (st', nba') ← execStmt2 fuel st nba s
        execStmts2 fuel st' nba' rest


end

/-! ## Qualified-case obligations

`CaseCheck` is retained metadata.  A qualified case cannot silently acquire
plain-case semantics on a violating input: before a process executes, the
validator below checks the selected case's match set and returns a named loud
result when `unique`, `unique0`, or `priority` is violated.  Ordinary cases
take the old execution path unchanged. -/

mutual
def Stmt2.hasQualifiedCase : Stmt2 → Bool
  | .caseStmt _ items dflt _ check =>
      check != .none
      || CaseItem2.listHasQualifiedCase items.toList
      || (match dflt with | some s => s.hasQualifiedCase | none => false)
  | .ifStmt _ t e => t.hasQualifiedCase || (match e with | some s => s.hasQualifiedCase | none => false)
  | .block body => Stmt2.listHasQualifiedCase body.toList
  | .blockingAssign _ _ | .nbaAssign _ _ | .unsupported _ _ => false

def Stmt2.listHasQualifiedCase : List Stmt2 → Bool
  | [] => false
  | s :: rest => s.hasQualifiedCase || Stmt2.listHasQualifiedCase rest

def CaseItem2.listHasQualifiedCase : List CaseItem2 → Bool
  | [] => false
  | .mk _ body :: rest => body.hasQualifiedCase || CaseItem2.listHasQualifiedCase rest
end

def caseHitRows2 (fuel : Nat) (st : SvState) (subj : LVec) (mode : CaseMode) :
    List CaseItem2 → Res (List (Bool × Stmt2))
  | [] => .ok []
  | .mk pats body :: rest => do
      let pvs ← evalExprs2 fuel st pats.toList
      let rows ← caseHitRows2 fuel st subj mode rest
      return (pvs.any (fun pv => caseHit mode subj pv), body) :: rows

def checkCaseObligation (check : CaseCheck) (hasDefault : Bool)
    (hits : List (Bool × Stmt2)) : Res Unit :=
  let n := (hits.filter (·.1)).length
  match check with
  | .none => .ok ()
  | .unique => if n == 1 || (n == 0 && hasDefault) then .ok () else .unsupported "CaseCheck:unique"
  | .unique0 => if n ≤ 1 then .ok () else .unsupported "CaseCheck:unique0"
  | .priority => if n ≥ 1 || hasDefault then .ok () else .unsupported "CaseCheck:priority"

mutual
def validateCaseChecks2 (fuel : Nat) (st : SvState) (stmt : Stmt2) : Res Unit :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
      match stmt with
      | .blockingAssign _ _ | .nbaAssign _ _ | .unsupported _ _ => .ok ()
      | .ifStmt cond th el => do
          let cv ← evalExpr2 fuel st cond
          if cv.condTrue then validateCaseChecks2 fuel st th
          else match el with | some s => validateCaseChecks2 fuel st s | none => .ok ()
      | .block body => validateCaseChecksList2 fuel st body.toList
      | .caseStmt subj items dflt mode check => do
          let sv ← evalExpr2 fuel st subj
          let hits ← caseHitRows2 fuel st sv mode items.toList
          checkCaseObligation check dflt.isSome hits
          match hits.find? (·.1) with
          | some (_, body) => validateCaseChecks2 fuel st body
          | none => match dflt with | some s => validateCaseChecks2 fuel st s | none => .ok ()

def validateCaseChecksList2 (fuel : Nat) (st : SvState) (ss : List Stmt2) : Res Unit :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 =>
      match ss with
      | [] => .ok ()
      | s :: rest => do
          validateCaseChecks2 fuel st s
          validateCaseChecksList2 fuel st rest
end

def validateProcessChecks2 (fuel : Nat) (st : SvState) : List Process2 → Res Unit
  | [] => .ok ()
  | p :: rest => do
      match p with
      | .alwaysComb body | .alwaysFF _ body | .alwaysPlain _ body
      | .alwaysFFR _ _ body _ =>
          if body.hasQualifiedCase then validateCaseChecks2 fuel st body else .ok ()
      | .assign _ _ | .unsupported _ _ => .ok ()
      validateProcessChecks2 fuel st rest

/-! ## Phase classification -/

/-- Comb-phase processes (`unsupported` scheduled here for loudness — the
M0 rule). -/
def Process2.isCombPhase : Process2 → Bool
  | .alwaysComb _ | .assign _ _ | .unsupported _ _ => true
  | .alwaysFF _ _ | .alwaysPlain _ _ | .alwaysFFR _ _ _ _ => false

/-- Edge-phase processes: every posedge-clocked block, INCLUDING the
async-reset ones (the posedge fires with reset asserted too — the body's
reset branch is what holds the state, pin3 CYCLE 4). -/
def Process2.isEdgePhase : Process2 → Bool
  | .alwaysFF _ _ | .alwaysPlain _ _ | .alwaysFFR _ _ _ _ => true
  | _ => false

def Process2.hasQualifiedCase : Process2 → Bool
  | .alwaysComb body | .alwaysFF _ body | .alwaysPlain _ body | .alwaysFFR _ _ body _ => body.hasQualifiedCase
  | .assign _ _ | .unsupported _ _ => false

def Design2.combIndices (d : Design2) : List Nat :=
  (List.range d.processes.size).filter fun i =>
    match d.processes[i]? with
    | some p => p.isCombPhase
    | none => false

def Design2.edgeIndices (d : Design2) : List Nat :=
  (List.range d.processes.size).filter fun i =>
    match d.processes[i]? with
    | some p => p.isEdgePhase
    | none => false

/-! ## Comb settle -/

/-- Run one comb-phase process (continuous assigns may target selects —
partial drivers read-modify-write their slice; several assigns driving
disjoint slices of one signal settle to the union, undriven bits stay x). -/
def runCombProcess2 (fuel : Nat) (st : SvState) : Process2 → Res SvState
  | .assign target value => do
      let v ← evalExpr2 fuel st value
      match target with
      | .concat _ =>
          let windows ← resolveConcatLhs2 fuel st target
          writeConcatLhs2 st v windows
      | _ =>
          let (n, win) ← resolveLhs2 fuel st target
          match win with
          | none => .ok st
          | some (lo, w) =>
              match SvState.lookup st n with
              | some old => .ok (SvState.set st n (old.insertAt lo (v.resizeU w)))
              | none => .unsupported s!"unknown assign target '{n}'"
  | .alwaysComb body => do
      let (st', nba) ← execStmt2 fuel st [] body
      match nba with
      | [] => .ok st'
      | (name, _) :: _ =>
          .unsupported
            s!"nonblocking assignment to '{name}' inside always_comb is outside the cycle semantics"
  | .alwaysFF _ _ | .alwaysPlain _ _ | .alwaysFFR _ _ _ _ => .ok st
  | .unsupported svKind _ => .unsupported s!"unsupported process '{svKind}'"

def combPass2 (d : Design2) (fuel : Nat) (st : SvState) : List Nat → Res SvState
  | [] => .ok st
  | i :: rest => do
      let st' ← match d.processes[i]? with
        | some p => runCombProcess2 fuel st p
        | none => pure st
      combPass2 d fuel st' rest

/-- σ-ordered settle passes to fixpoint, fuel-bounded (M0 discipline). -/
def combSettle2 (d : Design2) (σ : ScheduleOracle) (fuel : Nat) (st : SvState)
    (k : Nat) : Res (SvState × Nat) :=
  match fuel with
  | 0 => .timeout
  | fuel + 1 => do
      let st' ← combPass2 d fuel st (σ.choose k d.combIndices)
      if st' == st then .ok (st', k + 1)
      else combSettle2 d σ fuel st' (k + 1)

/-! ## Reset-event phase -/

/-! `isNegedge` and `SvState.bit0` were defined HERE and are now in `Basic`
and `Semantics` respectively. The region tier needs the same two, and it
sits BELOW this module in the import graph, so a copy was made — and the
copy disagreed with this original on the `x`/`z` cases. Moving them down
is what makes the two tiers agree by construction. Nothing else changes:
the names are unqualified and identical, so `resetIndices` below reads the
same rule it always did. -/

/-- Indices of `alwaysFFR` processes whose reset input made its configured
§9.4.2 edge between `prev` (end of previous cycle) and `cur` (this cycle's
settled inputs) — the ready set of the reset-event phase. -/
def Design2.resetIndices (d : Design2) (prev cur : SvState) : List Nat :=
  (List.range d.processes.size).filter fun i =>
    match d.processes[i]? with
    | some (.alwaysFFR _ rn _ resetHigh) =>
        if resetHigh then isPosedge (SvState.bit0 prev rn) (SvState.bit0 cur rn)
        else isNegedge (SvState.bit0 prev rn) (SvState.bit0 cur rn)
    | _ => false

/-- Run one process at a reset event (only `alwaysFFR` bodies are ever
listed). The WHOLE body runs — on `rst_n: 1→x` the canonical
`if (!rst_n)` takes the else branch at the event (pin3 CYCLE 6). -/
def runResetProcess2 (fuel : Nat) (st : SvState) (nba : NbaQueue2) :
    Process2 → Res (SvState × NbaQueue2)
  | .alwaysFFR _ _ body _ => execStmt2 fuel st nba body
  | _ => .ok (st, nba)

def resetPass2 (d : Design2) (fuel : Nat) (st : SvState) (nba : NbaQueue2) :
    List Nat → Res (SvState × NbaQueue2)
  | [] => .ok (st, nba)
  | i :: rest => do
      let (st', nba') ← match d.processes[i]? with
        | some p => runResetProcess2 fuel st nba p
        | none => pure (st, nba)
      resetPass2 d fuel st' nba' rest

/-! ## Edge phase -/

def runEdgeProcess2 (fuel : Nat) (st : SvState) (nba : NbaQueue2) :
    Process2 → Res (SvState × NbaQueue2)
  | .alwaysFF _ body => execStmt2 fuel st nba body
  | .alwaysPlain _ body => execStmt2 fuel st nba body
  | .alwaysFFR _ _ body _ => execStmt2 fuel st nba body
  | .alwaysComb _ | .assign _ _ => .ok (st, nba)
  | .unsupported svKind _ => .unsupported s!"unsupported process '{svKind}'"

def edgePass2 (d : Design2) (fuel : Nat) (st : SvState) (nba : NbaQueue2) :
    List Nat → Res (SvState × NbaQueue2)
  | [] => .ok (st, nba)
  | i :: rest => do
      let (st', nba') ← match d.processes[i]? with
        | some p => runEdgeProcess2 fuel st nba p
        | none => pure (st, nba)
      edgePass2 d fuel st' nba' rest

/-! ## The cycle -/

def applyInputs2 (d : Design2) (inputs : SvState) (st : SvState) : SvState :=
  d.decls.foldl
    (fun s declaration =>
      if declaration.isInput then
        match SvState.lookup inputs declaration.name with
        | some v => SvState.set s declaration.name
            (if declaration.twoState then v.toTwoState else v)
        | none => s
      else s)
    st

def initState2 (d : Design2) : SvState :=
  d.decls.toList.map fun dc =>
    let value := dc.init.getD (LVec.xVec dc.width)
    (dc.name, if dc.twoState then value.toTwoState else value)

/-- One clock cycle of the semantic tier — the M0 five sub-steps with the
reset-event phase inserted between the input settle and the posedge:

1. overwrite declared inputs from the stimulus;
2. comb settle;
3. **reset-event phase**: `alwaysFFR` bodies whose reset made a negedge
   (vs. the end of the previous cycle) run once in σ order, NBA committed,
   comb settled — reset values are visible to the posedge samplers (pin3);
4. edge phase: every posedge process runs once in σ order;
5. NBA commit; 6. comb settle. The result is the cycle's snapshot. -/
def cycleStep2 (d : Design2) (σ : ScheduleOracle) (fuel : Nat) (inputs : SvState)
    (st : SvState) (k : Nat := 0) : Res (SvState × Nat) := do
  let st0 := applyInputs2 d inputs st
  let (st1, k1) ← combSettle2 d σ fuel st0 k
  let rdy := d.resetIndices st st1
  let (st2, k2) ←
    match rdy with
    | [] => pure (st1, k1)
    | _ => do
        let (str, nbar) ← resetPass2 d fuel st1 [] (σ.choose k1 rdy)
        combSettle2 d σ fuel (commitNba2 str nbar) (k1 + 1)
  let (st3, nba) ← edgePass2 d fuel st2 [] (σ.choose k2 d.edgeIndices)
  let st4 := commitNba2 st3 nba
  combSettle2 d σ fuel st4 (k2 + 1)

def runFrom2 (d : Design2) (σ : ScheduleOracle) (fuel : Nat) (st : SvState)
    (k : Nat) : List SvState → Res (List SvState)
  | [] => .ok []
  | inputs :: rest => do
      let (st', k') ← cycleStep2 d σ fuel inputs st k
      let tr ← runFrom2 d σ fuel st' k' rest
      return st' :: tr

def validateTraceChecks2 (fuel : Nat) (d : Design2) : List SvState → Res Unit
  | [] => .ok ()
  | st :: rest => do
      validateProcessChecks2 fuel st d.processes.toList
      validateTraceChecks2 fuel d rest

/-- The semantic-tier entry point (M0 contract shape: startup from
declaration initializers, all else x; one snapshot per stimulus entry). -/
def run2 (d : Design2) (σ : ScheduleOracle) (fuel : Nat) (stim : List SvState) :
    Res (List SvState) :=
  do
    let tr ← runFrom2 d σ fuel (initState2 d) 0 stim
    if d.processes.toList.any Process2.hasQualifiedCase then
      validateTraceChecks2 fuel d tr
    return tr

/-! ## The pinned-fact table (`#guard` twins of the diff rows)

Row ids refer to the pin probes in the module docstring; every row was
green on Xcelium AND Icarus before the rule landed (case-inside rows:
Xcelium only — Icarus cannot parse the construct). -/

-- pin1 R01–R03: dynamic bit select; x/z index reads x.
private def v86 : LVec := .lit "10100110"
#guard evalExpr2 32 [] (.index (.lit v86) (.lit (.ofNat 3 2)) 1) == .ok (.lit "1")
#guard evalExpr2 32 [] (.index (.lit v86) (.lit (.lit "xxx")) 1) == .ok (.lit "x")
#guard evalExpr2 32 [] (.index (.lit v86) (.lit (.lit "01x")) 1) == .ok (.lit "x")

-- pin1 R04–R05: 2D element select m[i] = 8-bit chunk; x index → all-x.
private def m4x8 : LVec := .lit "10100001101100101100001111010100"  -- 32'hA1B2C3D4
#guard (evalExpr2 32 [] (.index (.lit m4x8) (.lit (.ofNat 2 3)) 8)).toOption.map
        LVec.toBinString == some "10100001"
#guard (evalExpr2 32 [] (.index (.lit m4x8) (.lit (.lit "xx")) 8)).toOption.map
        LVec.toBinString == some "xxxxxxxx"

-- pin2a W05: out-of-range dynamic read → x.
#guard (evalExpr2 32 [] (.index (.lit v86) (.lit (.ofNat 4 9)) 1)) == .ok (.lit "x")

-- pin1 R06: constant part select v[6:3].
#guard (evalExpr2 32 [] (.slice (.lit v86) 3 4)).toOption.map LVec.toBinString
        == some "0100"

-- pin1 R07 / pin4 Z06: replication.
#guard (evalExpr2 32 [] (.repl 3 (.lit (.lit "10")))).toOption.map LVec.toBinString
        == some "101010"
#guard (evalExpr2 32 [] (.repl 2 (.lit (.lit "1x")))).toOption.map LVec.toBinString
        == some "1x1x"

-- pin1 R08–R11, pin4 Z07: reductions.
#guard evalRedOp .or (.lit "00000000") == .l0
#guard evalRedOp .or (.lit "000x0000") == .lx
#guard evalRedOp .or (.lit "100x0000") == .l1
#guard evalRedOp .and (.lit "11111111") == .l1
#guard evalRedOp .and (.lit "11111110") == .l0
#guard evalRedOp .and (.lit "1111111x") == .lx
#guard evalRedOp .xor (.lit "10110000") == .l1
#guard evalRedOp .xor (.lit "1011000x") == .lx
#guard evalRedOp .nor (.lit "00000000") == .l1
#guard evalRedOp .nand (.lit "11111111") == .l0
#guard evalRedOp .or (.lit "zz") == .lx
#guard evalRedOp .and (.lit "zz") == .lx

-- pin4 S01–S05: signed comparison; $signed at equal width is bit identity.
#guard (evalExpr2 32 [] (.scmp .lt (.lit (.ofNat 8 0xFF)) (.lit (.ofNat 8 1))))
        == .ok (.lit "1")
#guard (evalExpr2 32 [] (.binary .lt (.lit (.ofNat 8 0xFF)) (.lit (.ofNat 8 1))))
        == .ok (.lit "0")
#guard (evalExpr2 32 [] (.scmp .ge (.lit (.ofNat 8 0xFF)) (.lit (.ofNat 8 1))))
        == .ok (.lit "0")
#guard (evalExpr2 32 [] (.scmp .lt (.lit (.ofNat 8 0x80)) (.lit (.ofNat 8 0x7F))))
        == .ok (.lit "1")
#guard (evalExpr2 32 [] (.scmp .lt (.lit (.lit "x1111111")) (.lit (.ofNat 8 0x7F))))
        == .ok (.lit "x")

-- pin5 L01–L12: logical && / || (definite-false/true short-outs, else x).
#guard evalExpr2 8 [] (.logical true (.lit (.lit "00")) (.lit (.lit "xx"))) == .ok (.lit "0")
#guard evalExpr2 8 [] (.logical true (.lit (.lit "xx")) (.lit (.lit "00"))) == .ok (.lit "0")
#guard evalExpr2 8 [] (.logical true (.lit (.lit "10")) (.lit (.lit "xx"))) == .ok (.lit "x")
#guard evalExpr2 8 [] (.logical true (.lit (.lit "10")) (.lit (.lit "01"))) == .ok (.lit "1")
#guard evalExpr2 8 [] (.logical true (.lit (.lit "0x")) (.lit (.lit "10"))) == .ok (.lit "x")
#guard evalExpr2 8 [] (.logical true (.lit (.lit "zz")) (.lit (.lit "10"))) == .ok (.lit "x")
#guard evalExpr2 8 [] (.logical false (.lit (.lit "10")) (.lit (.lit "xx"))) == .ok (.lit "1")
#guard evalExpr2 8 [] (.logical false (.lit (.lit "xx")) (.lit (.lit "01"))) == .ok (.lit "1")
#guard evalExpr2 8 [] (.logical false (.lit (.lit "00")) (.lit (.lit "00"))) == .ok (.lit "0")
#guard evalExpr2 8 [] (.logical false (.lit (.lit "xx")) (.lit (.lit "00"))) == .ok (.lit "x")

-- pin4 Z01–Z04: bitwise resize (zero-extend keeps x bits; truncate keeps
-- low bits) — never the arithmetic collapse.
#guard (LVec.resizeU (.lit "1010") 12).toBinString == "000000001010"
#guard (LVec.resizeU (.lit "10100101") 4).toBinString == "0101"
#guard (LVec.resizeU (.lit "x1x0") 12).toBinString == "00000000x1x0"
#guard (LVec.resizeU (.lit "1x000101") 4).toBinString == "0101"

-- pin2a W01/W02/W06/W08 + pin4 Z05: select writes (x/z/OOB index = no-op).
private def wrProbe (idx : String) : Res (SvState × NbaQueue2) :=
  execStmt2 32 [("v", LVec.ofNat 8 0x0F)] []
    (.blockingAssign (.index "v" (.lit (.lit idx)) 1) (.lit (.lit "1")))
#guard (wrProbe "xxx").toOption.map (fun (st, _) => SvState.showSignal st "v")
        == some "00001111"
#guard (wrProbe "110").toOption.map (fun (st, _) => SvState.showSignal st "v")
        == some "01001111"
#guard (wrProbe "zzz").toOption.map (fun (st, _) => SvState.showSignal st "v")
        == some "00001111"
#guard (wrProbe "1x0").toOption.map (fun (st, _) => SvState.showSignal st "v")
        == some "00001111"
#guard (execStmt2 32 [("v", LVec.ofNat 8 0x0F)] []
          (.blockingAssign (.index "v" (.lit (.ofNat 5 20)) 1) (.lit (.lit "1")))).toOption.map
        (fun (st, _) => SvState.showSignal st "v") == some "00001111"

-- pin2a W03/W04: element write via x index no-op; known index hits chunk.
#guard (execStmt2 32 [("m", LVec.lit "00010001001000100011001101000100")] []
          (.blockingAssign (.index "m" (.lit (.lit "xx")) 8) (.lit (.ofNat 8 0xFF)))).toOption.map
        (fun (st, _) => SvState.showSignal st "m")
        == some "00010001001000100011001101000100"
#guard (execStmt2 32 [("m", LVec.lit "00010001001000100011001101000100")] []
          (.blockingAssign (.index "m" (.lit (.ofNat 2 1)) 8) (.lit (.ofNat 8 0xFF)))).toOption.map
        (fun (st, _) => SvState.showSignal st "m")
        == some "00010001001000101111111101000100"

-- A procedural concatenation target uses the same fixed-window splitter as a
-- continuous assignment. Right streaming in the symbolic surface lowers to
-- this target shape, including a flattened fixed-array cell.
#guard (execStmt2 32 [("arr[0]", LVec.xVec 4), ("out", LVec.xVec 1)] []
          (.blockingAssign (.concat [.ident "arr[0]", .ident "out"])
            (.lit (.lit "10101")))).toOption.map
        (fun (st, _) => (SvState.showSignal st "arr[0]", SvState.showSignal st "out"))
        == some ("1010", "1")

-- pin2a C01–C05: plain case = case equality (x matches x); expression
-- labels; unique does not change execution (first match; no match+no
-- default = no-op).
private def oSet (v : Nat) : Stmt2 := .blockingAssign (.ident "o") (.lit (.ofNat 2 v))
private def caseProbe (subj : String) (items : Array CaseItem2)
    (dflt : Option Stmt2) (mode : CaseMode := .normal) : Option String :=
  (execStmt2 64 [("s", LVec.lit subj), ("o", LVec.xVec 2)] []
    (.caseStmt (.ident "s") items dflt mode .none)).toOption.map
    fun (st, _) => SvState.showSignal st "o"
#guard caseProbe "xx"
        #[.mk #[.lit (.lit "00")] (oSet 0), .mk #[.lit (.lit "xx")] (oSet 1)]
        (some (oSet 2))
        == some "01"
#guard caseProbe "0x"
        #[.mk #[.lit (.lit "00")] (oSet 0), .mk #[.lit (.lit "01")] (oSet 1)]
        (some (oSet 3))
        == some "11"
-- fifo style: case (1'b1) with signal labels, first match wins.
#guard (execStmt2 64 [("s", LVec.lit "11"), ("p", LVec.xVec 3)] []
          (.caseStmt (.lit (.lit "1"))
            #[.mk #[.index (.ident "s") (.lit (.ofNat 1 0)) 1]
                (.blockingAssign (.ident "p") (.lit (.ofNat 3 1))),
              .mk #[.index (.ident "s") (.lit (.ofNat 1 1)) 1]
                (.blockingAssign (.ident "p") (.lit (.ofNat 3 2)))]
            (some (.blockingAssign (.ident "p") (.lit (.ofNat 3 7)))) .normal .unique)).toOption.map
        (fun (st, _) => SvState.showSignal st "p") == some "001"
#guard (execStmt2 64 [("s", LVec.lit "00"), ("p", LVec.ofNat 3 5)] []
          (.caseStmt (.lit (.lit "1"))
            #[.mk #[.index (.ident "s") (.lit (.ofNat 1 0)) 1]
                (.blockingAssign (.ident "p") (.lit (.ofNat 3 1))),
              .mk #[.index (.ident "s") (.lit (.ofNat 1 1)) 1]
                (.blockingAssign (.ident "p") (.lit (.ofNat 3 2)))]
            none .normal .unique)).toOption.map
        (fun (st, _) => SvState.showSignal st "p") == some "101"

-- pin2b I01–I09: case inside (wildcard match, item-side don't-cares;
-- subject x in a cared position falls through; comma lists).
#guard caseProbe "10"
        #[.mk #[.lit (.lit "0z")] (oSet 0), .mk #[.lit (.lit "1z")] (oSet 1)]
        (some (oSet 2)) .inside == some "01"
#guard caseProbe "0x"
        #[.mk #[.lit (.lit "00")] (oSet 0), .mk #[.lit (.lit "01")] (oSet 1)]
        (some (oSet 2)) .inside == some "10"
#guard caseProbe "1x"
        #[.mk #[.lit (.lit "0z")] (oSet 0), .mk #[.lit (.lit "1z")] (oSet 1)]
        (some (oSet 2)) .inside == some "01"
#guard caseProbe "10"
        #[.mk #[.lit (.lit "1x")] (oSet 1)]
        (some (oSet 2)) .inside == some "01"
#guard caseProbe "01"
        #[.mk #[.lit (.lit "00"), .lit (.lit "01")] (oSet 0)]
        (some (oSet 2)) .inside == some "00"
#guard caseProbe "z0"
        #[.mk #[.lit (.lit "00")] (oSet 0), .mk #[.lit (.lit "10")] (oSet 1)]
        (some (oSet 2)) .inside == some "10"
#guard caseProbe "x1"
        #[.mk #[.lit (.lit "z1")] (oSet 1)]
        (some (oSet 2)) .inside == some "01"

-- pin3: the async-reset design, all 8 pinned cycles (see the docstring's
-- reset ledger). Model of rows/pin3.sv `ar`.
private def arDesign : Design2 :=
  { name := "ar"
    decls := #[
      { name := "clk", width := 1, isInput := true },
      { name := "rst_n", width := 1, isInput := true },
      { name := "d", width := 4, isInput := true },
      { name := "q", width := 4, isOutput := true },
      { name := "q2", width := 4, isOutput := true }]
    processes := #[
      @Process2.alwaysFFR "clk" "rst_n"
        (.ifStmt (.unary .bnot (.ident "rst_n"))
          (.nbaAssign (.ident "q") (.lit (.ofNat 4 0)))
          (some (.nbaAssign (.ident "q") (.ident "d")))) false,
      .alwaysFF "clk" (.nbaAssign (.ident "q2") (.ident "q"))] }

private def arStim : List SvState := [
  [("rst_n", LVec.lit "0"), ("d", LVec.ofNat 4 5)],
  [("rst_n", LVec.lit "1"), ("d", LVec.ofNat 4 5)],
  [("d", LVec.ofNat 4 7)],
  [("rst_n", LVec.lit "0"), ("d", LVec.ofNat 4 9)],
  [("d", LVec.ofNat 4 3)],
  [("rst_n", LVec.lit "1")],
  [("rst_n", LVec.lit "x"), ("d", LVec.ofNat 4 6)],
  [("rst_n", LVec.lit "1"), ("d", LVec.ofNat 4 2)]]

#guard (Res.toOption (run2 arDesign σ_src 64 arStim)).map
        (fun tr => tr.map fun st =>
          (SvState.showSignal st "q", SvState.showSignal st "q2"))
      == some [
        ("0000", "0000"),  -- c0: x→0 negedge resets pre-edge; q2 samples 0
        ("0101", "0000"),  -- c1: release; q2 samples pre-edge q
        ("0111", "0101"),  -- c2
        ("0000", "0000"),  -- c3: 1→0 negedge — reset visible to q2 SAME cycle
        ("0000", "0000"),  -- c4: held low: posedge reset branch holds
        ("0011", "0000"),  -- c5: deassert = posedge of rst_n, no event
        ("0110", "0110"),  -- c6: 1→x negedge runs ELSE branch pre-edge!
        ("0010", "0110")]  -- c7: x→1 = posedge, no event

-- Reset phase and edge phase both fire for a held-low reset with data
-- changing (the level-idempotence half of the ledger): q stays 0.
#guard (Res.toOption (run2 arDesign σ_src 64
        [[("rst_n", LVec.lit "0"), ("d", LVec.ofNat 4 5)],
         [("d", LVec.ofNat 4 9)], [("d", LVec.ofNat 4 1)]])).map
        (fun tr => tr.map fun st => SvState.showSignal st "q")
      == some ["0000", "0000", "0000"]

-- Partial continuous drivers (ff_one shape): two assigns drive disjoint
-- bits of one net; undriven bits stay x.
private def partialDrv : Design2 :=
  { name := "pd"
    decls := #[
      { name := "a", width := 2, isInput := true },
      { name := "y", width := 4 }]
    processes := #[
      .assign (.slice "y" 0 1) (.index (.ident "a") (.lit (.ofNat 1 0)) 1),
      .assign (.slice "y" 2 1) (.index (.ident "a") (.lit (.ofNat 1 1)) 1)] }

#guard (Res.toOption (run2 partialDrv σ_src 64 [[("a", LVec.lit "11")]])).map
        (fun tr => tr.map fun st => SvState.showSignal st "y")
      == some ["x1x1"]

-- A concatenated continuous driver samples one source vector and distributes
-- its MSB-first leaves to the existing packed windows.
private def concatDrv : Design2 :=
  { name := "concat_drv"
    decls := #[
      { name := "source", width := 8, isInput := true },
      { name := "upper", width := 4 },
      { name := "lower", width := 4 }]
    processes := #[
      .assign (.concat [.ident "upper", .ident "lower"]) (.ident "source")] }

#guard (Res.toOption (run2 concatDrv σ_src 64
        [[("source", LVec.lit "00111100")],
         [("source", LVec.lit "10100101")],
         [("source", LVec.lit "xxxx0000")]])).map
        (fun tr => tr.map fun st =>
          (SvState.showSignal st "upper", SvState.showSignal st "lower"))
      == some [("0011", "1100"), ("1010", "0101"), ("xxxx", "0000")]

end LeanModels.Sv