LeanModels.Sv.Semantics
ls-sv semantics
- Lines
- 504
- file
LeanModels/Sv/Semantics.lean- sha256
06652c4cca2ccf5649ec3a491a73fce6d624d9fb51dbb466f643e66f8b3b6923- statements that rely on it
_example,add,aes_sbox,arbiter,clmul,fdivp,matmul,mul,sorter- download
LeanModels.Sv.Semantics.lean
LeanModels/Sv/Semantics.leanLeanModels.Sv.Semantics.lean
import LeanModels.Sv.Ast
/-!
# SV M0 cycle/scheduler semantics (`LeanModels.Sv`)
The executable cycle-level semantics of `docs/sv-design-m0.md` ("Scheduler
core M0"). A run is a *deterministic* function of
`(design, stimulus, σ : ScheduleOracle, fuel)`; all LRM same-region ordering
freedom is concentrated in σ, so `∀ σ` theorems mean "for all legal
schedules" and a concrete σ is an executable simulator.
## The schedule oracle
`ScheduleOracle` bundles a choice function with its legality proof:
* `choose : Nat → List Nat → List Nat` — invocation counter and the ready
list (indices into `Design.processes`) in, execution order out;
* `choose_perm` — every invocation yields a **permutation** of the ready
list (the LRM never lets a simulator drop or duplicate a ready process).
Bundling the proof makes `∀ σ` theorems quantify over exactly the legal
schedules with no side conditions. The invocation counter `k` is threaded
through the whole run (never reset at cycle boundaries) and incremented at
every oracle invocation — one invocation per comb-settle **pass** and one
per edge phase — so a schedule may order every region of every cycle
independently, which is the LRM's actual freedom. `σ_src` (source order,
`choose := fun _ ready => ready`) is the executable default and matches
Xcelium's empirical order on the M0 examples; `σ_rev` reverses every
invocation; `ScheduleOracle.revWhen p` reverses exactly the invocations
with `p k = true` (witness-builder for race theorems).
## One cycle (`cycleStep`, the contract's 5 sub-steps)
1. Overwrite **declared input ports** from the cycle's stimulus entry
(`applyInputs`). Stimulus names that are not input ports are ignored —
the stimulus can never clobber internal state (essential for `∀ stim`
theorems); an input absent from the entry holds its previous value.
2. Comb settle (`combSettle`): σ-ordered passes over the `always_comb` +
continuous-`assign` processes until a pass changes nothing; the pass
count is fuel-bounded, exhaustion = combinational loop = `.timeout`.
3. Edge phase (`edgePass`): every `@(posedge clk)` process runs **once**, in
σ order. Blocking assigns hit the state immediately (this is what makes
`race_blk` schedule-dependent); nonblocking assigns append to an NBA
queue, so their reads see the sequential (pre-commit) state — the LRM's
Active/NBA region split at cycle granularity.
4. NBA commit (`commitNba`) in queue order — last write to a name wins.
5. Comb settle again. The result is the cycle's trace snapshot.
`run` starts from `initState`: declaration initializers where present,
all-x otherwise (LRM §6.8 startup — verified: pre-reset `counter` is x).
## Semantic decisions beyond the contract text
* The clock is *implicit*: every `cycleStep` is one posedge of the (single)
M0 clock, and every edge process fires each cycle. The recorded `clock`
name and the value of a `clk` input in the state are not consulted.
* `unsupported` **processes** are scheduled in the comb phase
(`Process.isCombPhase`), so any `cycleStep` on a design containing one is
loud (`.unsupported`) instead of silently dropping the process (an
ignored `initial` block would be silently wrong). Unsupported
statements/expressions are loud only when actually *reached* — an
untaken `if` branch may contain them harmlessly.
* A nonblocking assign inside `always_comb` (legal SV, absent from the M0
examples) is `.unsupported` — its LRM semantics needs NBA scheduling
inside the settle loop, which is the next tier.
* Reading an identifier that is not in the state is `.unsupported` (cannot
arise from extracted designs — the extractor resolves all names).
* Fuel is a *depth* bound with the Python lane's discipline: every fueled
function matches fuel first (`0 => .timeout`) and passes the decremented
fuel to every recursive call (siblings share it). One `fuel` bounds both
expression/statement depth and the number of settle passes.
* Out-of-range process indices and mid-phase process-kind mismatches are
benign no-ops; `choose_perm` makes both unreachable — no theorem should
rely on the fallback.
-/
namespace LeanModels.Sv
/-! ## Results -/
/-- Interpreter results — the Python lane's `Res` minus the exception arm
(M0 SV has no exceptions). `unsupported` = outside the M0 tier (loud),
`timeout` = fuel exhausted (in particular: combinational loop in settle).
Unification with `Core/` is an integration-checklist item. -/
inductive Res (α : Type) where
| ok (a : α)
| timeout
| unsupported (msg : String)
deriving Repr, BEq, DecidableEq, Inhabited
instance : Monad Res where
pure := .ok
bind r f :=
match r with
| .ok a => f a
| .timeout => .timeout
| .unsupported msg => .unsupported msg
/-- Drop the failure information. -/
def Res.toOption : Res α → Option α
| .ok a => some a
| _ => none
/-! ## State -/
/-- Simulation state: association list signal-name ↦ current 4-state value
(same discipline as the Python lane's `Env`: first match wins, `set`
replaces in place). All writes target declared names, so a state built from
`initState` keeps declaration order forever — snapshots of different
schedules are comparable with `==`. `SvState` is an abbrev, so helpers must
be called by full name (`SvState.lookup st n`, not `st.lookup n`). -/
abbrev SvState := List (String × LVec)
/-- First match wins (there are no duplicates in states built by `run`). -/
def SvState.lookup : SvState → String → Option LVec
| [], _ => none
| (k, v) :: rest, name => if k == name then some v else SvState.lookup rest name
/-- Replace an existing binding in place, else append at the end. -/
def SvState.set : SvState → String → LVec → SvState
| [], name, v => [(name, v)]
| (k, w) :: rest, name, v =>
if k == name then (name, v) :: rest else (k, w) :: SvState.set rest name v
/-- The 1-bit view of a signal, for edge detection: bit 0, with an absent
or width-0 signal reading `x`.
Lives here, and not in `Sem2` where it was written, because BOTH tiers
read it — the M1 reset phase and the R1 region model's `sawEdge` — and
`Slot` sits below `Sem2` in the import graph. It was the second of two
duplicated spellings the edge rule carried; `isPosedge`/`isNegedge` in
`Basic` were the first. -/
def SvState.bit0 (st : SvState) (name : String) : Logic :=
match SvState.lookup st name with
| some v => v.bits[0]?.getD .lx
| none => .lx
/-- Display helper for tests/harness: the `%b` string of `name` in a state
(`"?"` when absent — cannot happen for declared signals). -/
def SvState.showSignal (st : SvState) (name : String) : String :=
match SvState.lookup st name with
| some v => v.toBinString
| none => "?"
/-- Pending nonblocking updates of the current edge phase, in issue order.
Committed by `commitNba`; last write to a name wins. -/
abbrev NbaQueue := List (String × LVec)
/-! ## The schedule oracle -/
/-- A legal schedule: at invocation `k`, orders the ready process list.
`choose_perm` is the legality proof — every invocation is a permutation of
the ready list — so `∀ σ : ScheduleOracle` ranges over exactly the legal
schedules. See the module docstring for the invocation-counter protocol. -/
structure ScheduleOracle where
/-- Invocation counter and ready process indices in, execution order out. -/
choose : Nat → List Nat → List Nat
/-- Every invocation yields a permutation of the ready list. -/
choose_perm : ∀ (k : Nat) (ready : List Nat), (choose k ready).Perm ready
/-- Source/declaration order — the executable default. Xcelium empirically
follows this order on the M0 examples. -/
def σ_src : ScheduleOracle where
choose := fun _ ready => ready
choose_perm := fun _ ready => List.Perm.refl ready
/-- Reverse order at every invocation — the canonical second `race_blk`
witness. -/
def σ_rev : ScheduleOracle where
choose := fun _ ready => ready.reverse
choose_perm := fun _ ready => List.reverse_perm ready
/-- Reverse exactly the invocations with `p k = true`, source order
elsewhere — builds targeted schedule witnesses for race theorems. -/
def ScheduleOracle.revWhen (p : Nat → Bool) : ScheduleOracle where
choose := fun k ready => if p k then ready.reverse else ready
choose_perm := fun k ready => by
split
· exact List.reverse_perm ready
· exact List.Perm.refl ready
instance : Inhabited ScheduleOracle := ⟨σ_src⟩
/-! ## Expression evaluation (pure helpers) -/
/-- Read a signal; an unknown identifier is outside the tier (loud), never
a default value. -/
def readSignal (st : SvState) (name : String) : Res LVec :=
match SvState.lookup st name with
| some v => .ok v
| none => .unsupported s!"unknown identifier '{name}' (not a declared signal)"
/-- M0 unary operator on an evaluated operand (dispatches into the
`Basic.lean` op library — there is no second operator table). -/
def evalUnaryOp : UnaryOp → LVec → LVec
| .bnot, v => v.not
| .lnot, v => .ofLogic v.lnot
| .neg, v => v.neg
| .pos, v => v.pos
/-- M0 binary operator on evaluated operands. 1-bit SV results
(`== != < <= > >=`) are lifted with `LVec.ofLogic`. -/
def evalBinOp : BinOp → LVec → LVec → LVec
| .add, a, b => a.add b
| .sub, a, b => a.sub b
| .mul, left, right => left.mul right
| .div, left, right => left.div right
| .sdiv, left, right => left.sdiv right
| .rem, left, right => left.rem right
| .srem, left, right => left.srem right
| .pow signedBase signedExponent, left, right => LVec.pow signedBase signedExponent left right
| .shl _, left, right => left.shl right
| .shr _, left, right => left.shr right
| .ashl _, left, right => left.ashl right
| .ashr signedLeft, left, right => left.ashr signedLeft right
| .and, a, b => a.and b
| .or, a, b => a.or b
| .xor, a, b => a.xor b
| .eq, a, b => .ofLogic (a.eqLogical b)
| .ne, a, b => .ofLogic (a.neLogical b)
| .caseEq, a, b => .ofLogic (a.eqCase b)
| .caseNe, a, b => .ofLogic (a.neCase b)
| .wildEq, a, b => .ofLogic (a.eqWildcard b)
| .wildNe, a, b => .ofLogic (a.neWildcard b)
| .casez, a, b => .ofLogic (if LVec.matchesCase false a b then .l1 else .l0)
| .casex, a, b => .ofLogic (if LVec.matchesCase true a b then .l1 else .l0)
| .land, a, b => .ofLogic (a.land b)
| .lor, a, b => .ofLogic (a.lor b)
| .lt, a, b => .ofLogic (a.lt b)
| .le, a, b => .ofLogic (a.le b)
| .gt, a, b => .ofLogic (a.gt b)
| .ge, a, b => .ofLogic (a.ge b)
| .slt, a, b => .ofLogic (a.slt b)
| .sle, a, b => .ofLogic (a.sle b)
| .sgt, a, b => .ofLogic (a.sgt b)
| .sge, a, b => .ofLogic (a.sge b)
section CaseEqualityGuards
#guard evalBinOp .caseEq (.lit "1x") (.lit "1x") == .lit "1"
#guard evalBinOp .caseEq (.lit "1x") (.lit "1z") == .lit "0"
#guard evalBinOp .caseNe (.lit "z") (.lit "x") == .lit "1"
#guard evalBinOp .land (.lit "00") (.lit "0x") == .lit "0"
#guard evalBinOp .lor (.lit "0x") (.lit "00") == .lit "x"
end CaseEqualityGuards
/-! ## The fueled interpreter core
Every function matches fuel first (`0 => .timeout`) and passes the
decremented fuel to every recursive call (the Python lane's discipline —
fuel is a depth bound, siblings share it; proofs do induction on fuel). -/
mutual
/-- Evaluate an expression in a state. M0 expressions are side-effect-free,
so a conditional evaluates **both** arms (`LVec.ternary` merges them when
the condition is ambiguous, §11.4.11) — an `unsupported` node in the
not-taken arm is therefore still loud, which is deliberately conservative. -/
def evalExpr (fuel : Nat) (st : SvState) (e : Expr) : 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 (← evalExpr fuel st a)
| .reduce op a => do
return .ofLogic (evalRedOp op (← evalExpr fuel st a))
| .resizeSigned width operand => do
return (← evalExpr fuel st operand).resizeSigned width
| .squash2 operand => do
return (← evalExpr fuel st operand).toTwoState
| .binary op l r => do
let a ← evalExpr fuel st l
let b ← evalExpr fuel st r
return evalBinOp op a b
| .ternary c t f => do
let cv ← evalExpr fuel st c
let tv ← evalExpr fuel st t
let fv ← evalExpr fuel st f
return LVec.ternary cv tv fv
| .concat parts => do
let vs ← evalExprs fuel st parts.toList
return LVec.concatMany vs.toArray
| .unsupported svKind _ => .unsupported s!"unsupported expression '{svKind}'"
/-- Evaluate a list of expressions left to right (concat parts, source
order — `parts[0]` most significant, per `LVec.concatMany`). -/
def evalExprs (fuel : Nat) (st : SvState) (es : List Expr) : Res (List LVec) :=
match fuel with
| 0 => .timeout
| fuel + 1 =>
match es with
| [] => .ok []
| e :: rest => do
let v ← evalExpr fuel st e
let vs ← evalExprs fuel st rest
return v :: vs
/-- Execute one statement: blocking assigns update the state immediately;
nonblocking assigns append `(target, value-now)` to the NBA queue (the value
is evaluated against the *current* state, commitment happens later);
an `if` with an untrue condition and no else is a no-op (§12.4 — an x/z
condition HOLDS targets, latch-style). -/
def execStmt (fuel : Nat) (st : SvState) (nba : NbaQueue) (stmt : Stmt) :
Res (SvState × NbaQueue) :=
match fuel with
| 0 => .timeout
| fuel + 1 =>
match stmt with
| .blockingAssign target value => do
let v ← evalExpr fuel st value
return (SvState.set st target v, nba)
| .nbaAssign target value => do
let v ← evalExpr fuel st value
return (st, nba ++ [(target, v)])
| .ifStmt cond thenBranch elseBranch => do
let c ← evalExpr fuel st cond
if c.condTrue then
execStmt fuel st nba thenBranch
else
match elseBranch with
| some s => execStmt fuel st nba s
| none => .ok (st, nba)
| .block body => execStmts fuel st nba body.toList
| .unsupported svKind _ => .unsupported s!"unsupported statement '{svKind}'"
/-- Execute statements in order, threading state and NBA queue. -/
def execStmts (fuel : Nat) (st : SvState) (nba : NbaQueue) (ss : List Stmt) :
Res (SvState × NbaQueue) :=
match fuel with
| 0 => .timeout
| fuel + 1 =>
match ss with
| [] => .ok (st, nba)
| s :: rest => do
let (st', nba') ← execStmt fuel st nba s
execStmts fuel st' nba' rest
end
/-! ## Phase classification -/
/-- Comb-phase processes: `always_comb` and continuous `assign` — plus
`unsupported` processes, which are scheduled here so that any `cycleStep`
on a design containing one is loud instead of silently dropping it. -/
def Process.isCombPhase : Process → Bool
| .alwaysComb _ | .assign _ _ | .unsupported _ _ => true
| .alwaysFF _ _ | .alwaysPlain _ _ => false
/-- Edge-phase processes: `always_ff @(posedge c)` and
`always @(posedge c)` (identical M0 cycle semantics). -/
def Process.isEdgePhase : Process → Bool
| .alwaysFF _ _ | .alwaysPlain _ _ => true
| _ => false
/-- Indices into `d.processes` of the comb-phase ready set, source order —
what the oracle permutes at each settle pass. -/
def Design.combIndices (d : Design) : List Nat :=
(List.range d.processes.size).filter fun i =>
match d.processes[i]? with
| some p => p.isCombPhase
| none => false
/-- Indices into `d.processes` of the edge-phase ready set, source order —
what the oracle permutes at the edge phase. -/
def Design.edgeIndices (d : Design) : List Nat :=
(List.range d.processes.size).filter fun i =>
match d.processes[i]? with
| some p => p.isEdgePhase
| none => false
/-! ## Comb settle (contract sub-steps 2 and 5) -/
/-- Run one comb-phase process. Edge processes are a no-op here (unreachable
from `Design.combIndices`); `unsupported` processes are loud. A nonblocking
assign inside `always_comb` is outside the M0 cycle semantics (see module
docstring). -/
def runCombProcess (fuel : Nat) (st : SvState) : Process → Res SvState
| .assign target value => do
let v ← evalExpr fuel st value
return SvState.set st target v
| .alwaysComb body => do
let (st', nba) ← execStmt fuel st [] body
match nba with
| [] => .ok st'
| (name, _) :: _ =>
.unsupported
s!"nonblocking assignment to '{name}' inside always_comb is outside the M0 cycle semantics"
| .alwaysFF _ _ | .alwaysPlain _ _ => .ok st
| .unsupported svKind _ => .unsupported s!"unsupported process '{svKind}'"
/-- One settle pass: run the listed comb-phase processes once, left to
right. Out-of-range indices are skipped (unreachable for oracle orders —
they are permutations of `Design.combIndices`). -/
def combPass (d : Design) (fuel : Nat) (st : SvState) : List Nat → Res SvState
| [] => .ok st
| i :: rest => do
let st' ← match d.processes[i]? with
| some p => runCombProcess fuel st p
| none => pure st
combPass d fuel st' rest
/-- Comb settle: σ-ordered passes until one changes nothing. Every pass is
one oracle invocation (`k` increments even on the final, stable pass). Fuel
bounds the pass count — exhaustion means a combinational loop
(`.timeout`). Returns the settled state and the next invocation counter. -/
def combSettle (d : Design) (σ : ScheduleOracle) (fuel : Nat) (st : SvState)
(k : Nat) : Res (SvState × Nat) :=
match fuel with
| 0 => .timeout
| fuel + 1 => do
let st' ← combPass d fuel st (σ.choose k d.combIndices)
if st' == st then .ok (st', k + 1)
else combSettle d σ fuel st' (k + 1)
/-! ## Edge phase and NBA commit (contract sub-steps 3 and 4) -/
/-- Run one edge-phase process (comb processes are a no-op here,
unreachable from `Design.edgeIndices`). `always_ff` and plain `always`
posedge blocks are semantically identical in M0. -/
def runEdgeProcess (fuel : Nat) (st : SvState) (nba : NbaQueue) :
Process → Res (SvState × NbaQueue)
| .alwaysFF _ body => execStmt fuel st nba body
| .alwaysPlain _ body => execStmt fuel st nba body
| .alwaysComb _ | .assign _ _ => .ok (st, nba)
| .unsupported svKind _ => .unsupported s!"unsupported process '{svKind}'"
/-- The edge phase: run the listed edge processes once each, left to right,
threading the sequential state (blocking assigns are immediately visible to
later processes in the order — the `race_blk` mechanism) and the NBA queue
(nonblocking reads see pre-commit values — the `swap_nba` mechanism). -/
def edgePass (d : Design) (fuel : Nat) (st : SvState) (nba : NbaQueue) :
List Nat → Res (SvState × NbaQueue)
| [] => .ok (st, nba)
| i :: rest => do
let (st', nba') ← match d.processes[i]? with
| some p => runEdgeProcess fuel st nba p
| none => pure (st, nba)
edgePass d fuel st' nba' rest
/-- Commit the NBA queue in issue order; sequential `SvState.set` makes the
last write to a name win. -/
def commitNba (st : SvState) (nba : NbaQueue) : SvState :=
nba.foldl (fun s u => SvState.set s u.1 u.2) st
/-! ## The cycle (contract sub-step 1 + assembly) -/
/-- Overwrite **declared input ports** from the cycle's stimulus entry.
Only names in `d.inputNames` are consulted: the stimulus can never touch
internal state or outputs, and an input absent from the entry holds its
previous value. Two-state input variables convert x/z bits to zero before
any process evaluates an expression. -/
def applyInputs (d : Design) (inputs : SvState) (st : SvState) : SvState :=
d.decls.foldl
(fun state declaration =>
if declaration.isInput then
match SvState.lookup inputs declaration.name with
| some value => SvState.set state declaration.name
(if declaration.twoState then value.toTwoState else value)
| none => state
else state)
st
/-- LRM startup state (§6.8): declaration initializers where present,
all-x otherwise, converting two-state variables to zero in declaration order. -/
def initState (d : Design) : 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 — exactly the contract's 5 sub-steps. `k` is the oracle
invocation counter carried across cycles (default `0` gives the
contract-shaped call `cycleStep d σ fuel inputs s`); the settled state is
the cycle's trace snapshot, returned with the next counter. -/
def cycleStep (d : Design) (σ : ScheduleOracle) (fuel : Nat) (inputs : SvState)
(st : SvState) (k : Nat := 0) : Res (SvState × Nat) := do
let st0 := applyInputs d inputs st -- 1. inputs
let (st1, k1) ← combSettle d σ fuel st0 k -- 2. comb settle
let (st2, nba) ← edgePass d fuel st1 [] (σ.choose k1 d.edgeIndices) -- 3. edge phase
let st3 := commitNba st2 nba -- 4. NBA commit
combSettle d σ fuel st3 (k1 + 1) -- 5. comb settle
/-- Run the remaining stimulus from a given state and invocation counter —
one snapshot per stimulus entry (`run` unfolds to this; induction over
cycles is induction over `stim` with `st`/`k` generalized). -/
def runFrom (d : Design) (σ : ScheduleOracle) (fuel : Nat) (st : SvState)
(k : Nat) : List SvState → Res (List SvState)
| [] => .ok []
| inputs :: rest => do
let (st', k') ← cycleStep d σ fuel inputs st k
let tr ← runFrom d σ fuel st' k' rest
return st' :: tr
/-- The contract entry point: initial state from declaration initializers
(all other signals all-x), one `cycleStep` and one trace snapshot per
stimulus entry. -/
def run (d : Design) (σ : ScheduleOracle) (fuel : Nat) (stim : List SvState) :
Res (List SvState) :=
runFrom d σ fuel (initState d) 0 stim
end LeanModels.Sv