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