Tasks / trusted base

LeanModels.Sv.Spec2

ls-sv semantics

Lines
522
file
LeanModels/Sv/Spec2.lean
sha256
a2836f647332ca6da8f21ea8f5309797116bdf2db77b9ec65a27c214ece026d9
statements that rely on it
_example, add, aes_sbox, arbiter, clmul, fdivp, matmul, mul, sorter
download
LeanModels.Sv.Spec2.lean
LeanModels/Sv/Spec2.leanLeanModels.Sv.Spec2.lean
import LeanModels.Sv.Sem2
import LeanModels.Sv.Obs

/-!
# The Design2 spec judgment (`LeanModels.Sv` — `⊨₂`)

The semantic tier (`Sem2.lean`) has an interpreter, `run2`, but until this
file no judgment a theorem could state about it. This is the Design2 twin of
M0's `Surface.lean` judgment layer, and it obeys the same laws:

* **Fuel never reaches a statement.** `Runs2 d σ stim tr` (`∃ fuel, run2 …
  = .ok tr`) is the one fuel boundary. Every public judgment — `Models2`
  (`d ⊨₂ P`), `spec2`, `Terminates2` — is built on it, so a spec mentions
  neither fuel nor the AST. Fuel appears in this file only inside
  `Runs2`'s definition, in the STRUCTURAL lemmas that open a run
  (`runFrom2_*`, `Runs2.snapshot`, `Runs2.snapshot_step` — existentially) or
  build one (`Runs2.intro`, `runFrom2_ok_of_invariant`,
  `run2_ok_of_invariant`, `Runs2.of_invariant`, `Terminates2.of_invariant` —
  as the invariant's step fuel), and in the bridges between a concrete
  interpreter run and the judgments: `Models2.of_run` (introduction from a
  fact about every fuel), `Models2.run` (elimination against one fuel), and
  the step hypothesis of `Runs2.forall_of_step` / `forall_mem_of_step` (a
  property preserved by every `.ok` `cycleStep2`, at any fuel).
* **`⊨₂` is all-schedule, all-stimulus, completed-run.** `d ⊨₂ P` says: for
  every legal schedule σ, every stimulus, and every trace that SOME fuel
  completes, `P d stim tr`. It does not say a run completes — a design that
  never settles satisfies every `⊨₂` vacuously. `Terminates2 d` is the
  non-vacuity half, and a `⊨₂` theorem is only meaningful beside it
  (`Models2.exists_of_terminates` packages the pair: a trace exists AND it
  satisfies `P`).
* **No fuel monotonicity in this file.** It neither needs nor imports it:
  `Models2` quantifies over every completing fuel, and the snapshot
  decomposition reads one run. That `Runs2` is functional across fuels (one
  trace per schedule and stimulus) is `Runs2.functional` in `Mono2.lean`,
  which also proves `run2_det` and `Terminates2.existsUnique`.
* **`P` sees the design** (`TraceProp2 = Design2 → stim → trace → Prop`), so
  property builders may mention `initState2 d`; design-agnostic properties
  are lifted with `spec2`.

## Stimulus conventions

A stimulus is a list of `SvState`s, one entry per cycle, and `cycleStep2`
applies entry `c` with `applyInputs2` before cycle `c` settles. A `⊨₂`
statement over stimuli should be written with these rules in mind, because
`applyInputs2` never fails:

* **Entries are applied verbatim.** The value bound to an input is stored
  as given, AT ITS OWN WIDTH; it is not resized to the port's declared width
  (SV port assignment would zero-extend). On the 4-bit `ripple_add` with
  `b = 0001` and `cin = 0`, the entry `a := LVec.ofNat 2 3` makes `a` the
  2-bit `11`, so the out-of-range reads `a[2]`, `a[3]` are `x` and `s`
  comes out `xx00`, where SV would give `0100` (pinned, with the two
  ignored-key cases below, by `#guard` rows in
  `Examples/system-verilog/ripple_add/spec.lean`).
  The one exception is a 2-STATE input declaration (`Decl.twoState`, the
  symbolic `bit`/`bit [W-1:0]` ports): `applyInputs2` maps its x/z bits to 0
  at the boundary (§6.3.1). The comb proof layer (`CombDesign.inFourState`)
  covers 4-state input ports only, where "verbatim" is exact.
* **Only declared inputs are read.** `applyInputs2` walks the input
  declarations (`d.inputNames`); a key that names no input — a misspelling such as `"A"`,
  or an output or internal net such as `"s"` — is silently ignored. If an
  entry binds a name twice, the first binding wins (`SvState.lookup`).
* **Unbound inputs hold.** An input the entry does not bind keeps its value
  from the previous snapshot; before any entry binds it, that is its
  `initState2` value (its initializer, else all-`x`).

A statement whose stimulus hypothesis is built at the wrong width or under a
misspelled name therefore does not say what it appears to: depending on its
form it holds vacuously (no run meets the hypothesis) or is false (the design
sees `x` or a narrower port). Challenge statements should quantify over
known values at the DECLARED width —
`SvState.lookup inputs "a" = some (LVec.ofBitVec x)` with
`x : BitVec <declared width>` (the shape of `ripple_correct_stim` in
`Examples/system-verilog/ripple_add/spec.lean`) — so that the hypothesis
fixes the width and names the port.

What the later proof layers consume:

* `Runs2.snapshot` / `Runs2.snapshot_step` — every snapshot is an `.ok`
  `cycleStep2` from the previous snapshot (or `initState2 d` at cycle 0),
  at the cycle's stimulus entry. The combinational settle theorem
  (`Comb2.lean`) lifts its per-settle equations to every snapshot through
  this, by induction on the cycle index (`snapshot_step` hands the previous
  snapshot back as a `tr[c]? = some prev` fact, the shape that induction
  needs).
* `run2_ok_of_invariant` / `Terminates2.of_invariant` — the introduction
  side: an invariant that holds initially and that every `cycleStep2` (at
  one fuel `F`, any inputs, any schedule counter) succeeds from and
  preserves yields a completed run, hence `Terminates2`.

The `#guard`/`example` section at the end runs a hand-built one-gate design
through `run2` and exercises both directions of `⊨₂` on it, and proves
`Terminates2` of a process-free design through the invariant helper.

This file imports `Obs.lean` (besides `Sem2.lean`) only for the `Res`
do-notation simp set (`Res.bind_eq_ok`, `Res.ok_bind`, …), the
`LawfulBEq LVec` instance, and `ScheduleOracle.choose_nil` (used by the
`Terminates2` example); it re-declares none of them, so it composes with
any file that imports both.

**Naming trap for later `Models2.*` declarations.** Lean opens the
`Models2` namespace while elaborating a declaration named `Models2.foo`, so
inside it `Models2.and` (the `⊨₂` conjunction rule) hides the root `and`
(Bool).
In `Models2.*` declarations, write `&&` / `Bool.and` and `Bool.true`
explicitly. No other declaration here shadows a root name. The trivial-spec
lemma is called `Models2.spec2_true`, not `Models2.true` or
`Models2.trivial`, because those names would hide `true` and `trivial`.
-/

namespace LeanModels.Sv

/-! ## The judgments -/

/-- A cycle-level trace property of a semantic-tier design: sees the design
(so property builders can mention `initState2 d`), the stimulus, and the
trace (one snapshot per stimulus entry). Design-agnostic properties are
lifted with `spec2`. -/
abbrev TraceProp2 := Design2 → List SvState → List SvState → Prop

/-- The fuel-abstracted Design2 run judgment: *some* fuel completes the run
of `d` under schedule `σ` and stimulus `stim` with trace `tr`. The only place
fuel appears in a Design2 spec. -/
def Runs2 (d : Design2) (σ : ScheduleOracle) (stim tr : List SvState) : Prop :=
  ∃ fuel, run2 d σ fuel stim = .ok tr

theorem run2_from_ok {d : Design2} {σ : ScheduleOracle} {fuel : Nat}
    {stim tr : List SvState} (h : run2 d σ fuel stim = .ok tr) :
    runFrom2 d σ fuel (initState2 d) 0 stim = .ok tr := by
  simp only [run2] at h
  obtain ⟨tr', hrun, hv⟩ := Res.bind_eq_ok.mp h
  by_cases hq : d.processes.toList.any Process2.hasQualifiedCase = true
  · simp only [if_pos hq] at hv
    obtain ⟨u, _, hu⟩ := Res.bind_eq_ok.mp hv
    have heq : tr' = tr := Res.ok.inj hu
    simpa [heq] using hrun
  · simp only [if_neg hq] at hv
    have heq : tr' = tr := Res.ok.inj hv
    simpa [heq] using hrun

/-- `d ⊨₂ P` — for **every** legal schedule, every stimulus, and every
completed run (`Runs2`), the trace satisfies `P`. Introduce from a raw
∀-fuel fact with `Models2.of_run`; eliminate against a concrete run with
`Models2.run` / `Models2.runs`. Says nothing about whether runs complete —
pair it with `Terminates2`. -/
def Models2 (d : Design2) (P : TraceProp2) : Prop :=
  ∀ (σ : ScheduleOracle) (stim tr : List SvState), Runs2 d σ stim tr → P d stim tr

@[inherit_doc] scoped infix:50 " ⊨₂ " => Models2

/-- Lift a design-agnostic stimulus/trace predicate into `⊨₂`
(`d ⊨₂ spec2 fun stim tr => …`). -/
def spec2 (P : List SvState → List SvState → Prop) : TraceProp2 := fun _ stim tr => P stim tr

/-- Non-vacuity: every schedule and every stimulus completes. A `⊨₂` theorem
is only meaningful together with this (a design that never settles satisfies
every `⊨₂`). -/
def Terminates2 (d : Design2) : Prop :=
  ∀ (σ : ScheduleOracle) (stim : List SvState), ∃ tr, Runs2 d σ stim tr

/-! ## `Runs2` basics -/

/-- Introduce `Runs2` from one concrete completing fuel. -/
theorem Runs2.intro {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    (fuel : Nat) (h : run2 d σ fuel stim = .ok tr) : Runs2 d σ stim tr :=
  ⟨fuel, h⟩

/-! ### Structural `runFrom2` lemmas (fuel-explicit; the `Runs2` lemmas below
hide the fuel again) -/

/-- The empty stimulus completes immediately with the empty trace (any fuel). -/
@[simp] theorem runFrom2_nil (d : Design2) (σ : ScheduleOracle) (fuel : Nat)
    (st : SvState) (k : Nat) : runFrom2 d σ fuel st k [] = .ok [] := by
  simp [runFrom2]

/-- One cycle of `runFrom2`, inverted: a completed run of `inputs :: rest`
is an `.ok` `cycleStep2` followed by a completed run of `rest` from its
result. -/
theorem runFrom2_cons_eq_ok {d : Design2} {σ : ScheduleOracle} {fuel : Nat}
    {st : SvState} {k : Nat} {inputs : SvState} {rest tr : List SvState} :
    runFrom2 d σ fuel st k (inputs :: rest) = .ok tr ↔
      ∃ st' k' tr', cycleStep2 d σ fuel inputs st k = .ok (st', k') ∧
        runFrom2 d σ fuel st' k' rest = .ok tr' ∧ tr = st' :: tr' := by
  simp only [runFrom2, Res.bind_eq_ok]
  constructor
  · rintro ⟨⟨st', k'⟩, h1, tr', h2, h3⟩
    exact ⟨st', k', tr', h1, h2, by simpa [eq_comm] using h3⟩
  · rintro ⟨st', k', tr', h1, h2, rfl⟩
    exact ⟨(st', k'), h1, tr', h2, rfl⟩

/-- A completed `runFrom2` emits one snapshot per stimulus entry. -/
theorem runFrom2_length {d : Design2} {σ : ScheduleOracle} {fuel : Nat} :
    ∀ {stim : List SvState} {st : SvState} {k : Nat} {tr : List SvState},
      runFrom2 d σ fuel st k stim = .ok tr → tr.length = stim.length
  | [], st, k, tr, h => by
      simp only [runFrom2_nil, Res.ok.injEq] at h
      subst h; rfl
  | inputs :: rest, st, k, tr, h => by
      obtain ⟨st', k', tr', _, h2, rfl⟩ := runFrom2_cons_eq_ok.mp h
      simp [runFrom2_length h2]

/-- Snapshot decomposition of a completed `runFrom2`: snapshot `c` is an
`.ok` `cycleStep2` (at the run's fuel) from the previous snapshot — or the
start state `st` at `c = 0` — at stimulus entry `c`. -/
theorem runFrom2_snapshot {d : Design2} {σ : ScheduleOracle} {fuel : Nat} :
    ∀ {stim : List SvState} {st : SvState} {k : Nat} {tr : List SvState},
      runFrom2 d σ fuel st k stim = .ok tr →
      ∀ {c : Nat} {s : SvState}, tr[c]? = some s →
        ∃ inputs prev kc kc', stim[c]? = some inputs ∧ (st :: tr)[c]? = some prev ∧
          cycleStep2 d σ fuel inputs prev kc = .ok (s, kc')
  | [], st, k, tr, h, c, s, hc => by
      simp only [runFrom2_nil, Res.ok.injEq] at h
      subst h; simp at hc
  | inputs :: rest, st, k, tr, h, c, s, hc => by
      obtain ⟨st', k', tr', h1, h2, rfl⟩ := runFrom2_cons_eq_ok.mp h
      cases c with
      | zero =>
          simp only [List.getElem?_cons_zero, Option.some.injEq] at hc
          subst hc
          exact ⟨inputs, st, k, k', rfl, rfl, h1⟩
      | succ c =>
          simp only [List.getElem?_cons_succ] at hc
          obtain ⟨inp, prev, kc, kc', hi, hp, hs⟩ := runFrom2_snapshot h2 hc
          exact ⟨inp, prev, kc, kc', by simpa using hi, by simpa using hp, hs⟩

/-! ### The `Runs2` structure lemmas -/

/-- A completed run has exactly one snapshot per stimulus entry. -/
theorem Runs2.length {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    (h : Runs2 d σ stim tr) : tr.length = stim.length := by
  obtain ⟨fuel, hrun⟩ := h
  exact runFrom2_length (run2_from_ok hrun)

/-- Every stimulus cycle of a completed run has a snapshot. -/
theorem Runs2.exists_snapshot {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    (h : Runs2 d σ stim tr) {c : Nat} (hc : c < stim.length) : ∃ st, tr[c]? = some st :=
  ⟨tr[c]'(h.length ▸ hc), List.getElem?_eq_getElem _⟩

/-- The empty stimulus completes with the empty trace, and only with it. -/
theorem Runs2.nil_iff {d : Design2} {σ : ScheduleOracle} {tr : List SvState} :
    Runs2 d σ [] tr ↔ tr = [] := by
  constructor
  · intro h; simpa using h.length
  · rintro rfl
    refine ⟨0, ?_⟩
    simp only [run2, runFrom2_nil, Res.ok_bind]
    simp [validateTraceChecks2]

/-- **Snapshot decomposition, step form**: snapshot `c` of a completed run
is an `.ok` `cycleStep2` from the previous snapshot — read off the shifted
trace `initState2 d :: tr`, so cycle 0 starts from `initState2 d` and cycle
`c + 1` from `tr[c]` — at the stimulus entry of cycle `c`. Fuel and the
schedule counters stay existential. This is the induction-friendly form:
at `c + 1` the previous snapshot comes back as `tr[c]? = some prev`. -/
theorem Runs2.snapshot_step {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    (h : Runs2 d σ stim tr) {c : Nat} {st : SvState} (hc : tr[c]? = some st) :
    ∃ fuel inputs prev k k', stim[c]? = some inputs ∧
      (initState2 d :: tr)[c]? = some prev ∧
      cycleStep2 d σ fuel inputs prev k = .ok (st, k') := by
  obtain ⟨fuel, hrun⟩ := h
  obtain ⟨inputs, prev, k, k', hi, hp, hs⟩ :=
    runFrom2_snapshot (run2_from_ok hrun) hc
  exact ⟨fuel, inputs, prev, k, k', hi, hp, hs⟩

/-- **Snapshot decomposition** (the closed form): every snapshot is a
successful `cycleStep2` from the previous snapshot (or `initState2 d` at
cycle 0), at the cycle's stimulus entry. `Runs2.snapshot_step` is the same
fact with the previous snapshot as a `getElem?` equation. -/
theorem Runs2.snapshot {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    (h : Runs2 d σ stim tr) {c : Nat} {st : SvState} (hc : tr[c]? = some st) :
    ∃ fuel inputs prev k k', stim[c]? = some inputs ∧
      (prev = if c = 0 then initState2 d else (tr[c-1]?.getD [])) ∧
      cycleStep2 d σ fuel inputs prev k = .ok (st, k') := by
  obtain ⟨fuel, inputs, prev, k, k', hi, hp, hs⟩ := h.snapshot_step hc
  refine ⟨fuel, inputs, prev, k, k', hi, ?_, hs⟩
  cases c with
  | zero => simpa [eq_comm] using hp
  | succ c => simp only [List.getElem?_cons_succ] at hp; simp [hp]

/-! ## Building runs from a cycle invariant -/

/-- **Run construction**: if, at fuel `F`, every `cycleStep2` from a state
satisfying `I` (any inputs, any schedule counter) succeeds and lands in `I`,
then `runFrom2` from any `I`-state completes on every stimulus, and every
snapshot satisfies `I`. -/
theorem runFrom2_ok_of_invariant {d : Design2} {σ : ScheduleOracle} {F : Nat}
    (I : SvState → Prop)
    (hstep : ∀ (inputs st : SvState) (k : Nat), I st →
      ∃ st' k', cycleStep2 d σ F inputs st k = .ok (st', k') ∧ I st') :
    ∀ (stim : List SvState) (st : SvState) (k : Nat), I st →
      ∃ tr, runFrom2 d σ F st k stim = .ok tr ∧ ∀ s ∈ tr, I s
  | [], st, k, _ => ⟨[], by simp⟩
  | inputs :: rest, st, k, hI => by
      obtain ⟨st', k', h1, hI'⟩ := hstep inputs st k hI
      obtain ⟨tr', h2, hall⟩ := runFrom2_ok_of_invariant I hstep rest st' k' hI'
      refine ⟨st' :: tr', runFrom2_cons_eq_ok.mpr ⟨st', k', tr', h1, h2, rfl⟩, ?_⟩
      intro s hs
      rcases List.mem_cons.mp hs with rfl | hs
      · exact hI'
      · exact hall s hs

/-- `run2` form of `runFrom2_ok_of_invariant`: an invariant that holds at
`initState2 d` and that every `cycleStep2` at fuel `F` succeeds from and
preserves makes `run2 d σ F` complete on every stimulus. -/
theorem run2_ok_of_invariant {d : Design2} {σ : ScheduleOracle} {F : Nat}
    (I : SvState → Prop) (hinit : I (initState2 d))
    (hstep : ∀ (inputs st : SvState) (k : Nat), I st →
      ∃ st' k', cycleStep2 d σ F inputs st k = .ok (st', k') ∧ I st')
    (hnoq : d.processes.toList.any Process2.hasQualifiedCase = false)
    (stim : List SvState) :
    ∃ tr, run2 d σ F stim = .ok tr ∧ ∀ s ∈ tr, I s := by
  obtain ⟨tr, hraw, hall⟩ :=
    runFrom2_ok_of_invariant I hstep stim (initState2 d) 0 hinit
  refine ⟨tr, ?_, hall⟩
  simp [run2, hraw, hnoq]

/-- `Runs2` form of `run2_ok_of_invariant` (fuel hidden again). -/
theorem Runs2.of_invariant {d : Design2} {σ : ScheduleOracle} {F : Nat}
    (I : SvState → Prop) (hinit : I (initState2 d))
    (hstep : ∀ (inputs st : SvState) (k : Nat), I st →
      ∃ st' k', cycleStep2 d σ F inputs st k = .ok (st', k') ∧ I st')
    (hnoq : d.processes.toList.any Process2.hasQualifiedCase = false)
    (stim : List SvState) :
    ∃ tr, Runs2 d σ stim tr ∧ ∀ s ∈ tr, I s := by
  obtain ⟨tr, h, hall⟩ := run2_ok_of_invariant I hinit hstep hnoq stim
  exact ⟨tr, ⟨F, h⟩, hall⟩

/-- **Snapshot invariant (elimination side)**: if `I` holds at `initState2 d`
and every `.ok` `cycleStep2` (any fuel, inputs, schedule counter) from an
`I`-state lands in `I`, then every snapshot of a completed run satisfies `I`.
This packages the cycle induction over `Runs2.snapshot_step`, so consumers
do not have to repeat it. -/
theorem Runs2.forall_of_step {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    {I : SvState → Prop} (hinit : I (initState2 d))
    (hstep : ∀ (fuel : Nat) (inputs prev : SvState) (k : Nat) (st : SvState) (k' : Nat),
      I prev → cycleStep2 d σ fuel inputs prev k = .ok (st, k') → I st)
    (h : Runs2 d σ stim tr) : ∀ (c : Nat) (st : SvState), tr[c]? = some st → I st := by
  intro c
  induction c with
  | zero =>
      intro st hc
      obtain ⟨fuel, inputs, prev, k, k', _, hp, hs⟩ := h.snapshot_step hc
      simp only [List.getElem?_cons_zero, Option.some.injEq] at hp
      subst hp
      exact hstep fuel inputs _ k st k' hinit hs
  | succ c ih =>
      intro st hc
      obtain ⟨fuel, inputs, prev, k, k', _, hp, hs⟩ := h.snapshot_step hc
      simp only [List.getElem?_cons_succ] at hp
      exact hstep fuel inputs prev k st k' (ih prev hp) hs

/-- Membership form of `Runs2.forall_of_step`: every state in the trace of a
completed run satisfies the step invariant. -/
theorem Runs2.forall_mem_of_step {d : Design2} {σ : ScheduleOracle} {stim tr : List SvState}
    {I : SvState → Prop} (hinit : I (initState2 d))
    (hstep : ∀ (fuel : Nat) (inputs prev : SvState) (k : Nat) (st : SvState) (k' : Nat),
      I prev → cycleStep2 d σ fuel inputs prev k = .ok (st, k') → I st)
    (h : Runs2 d σ stim tr) : ∀ st ∈ tr, I st := by
  intro st hst
  obtain ⟨c, hc, rfl⟩ := List.getElem_of_mem hst
  exact h.forall_of_step hinit hstep c _ (List.getElem?_eq_getElem hc)

/-- **`Terminates2` from a cycle invariant**: if `I (initState2 d)` and, for
each schedule, some fuel makes every `cycleStep2` from an `I`-state succeed
into `I`, the design terminates on every schedule and stimulus. -/
theorem Terminates2.of_invariant {d : Design2} (I : SvState → Prop)
    (hinit : I (initState2 d))
    (hstep : ∀ σ : ScheduleOracle, ∃ F, ∀ (inputs st : SvState) (k : Nat), I st →
      ∃ st' k', cycleStep2 d σ F inputs st k = .ok (st', k') ∧ I st')
    (hnoq : d.processes.toList.any Process2.hasQualifiedCase = false) :
    Terminates2 d := by
  intro σ stim
  obtain ⟨F, hF⟩ := hstep σ
  obtain ⟨tr, h, _⟩ := Runs2.of_invariant (F := F) I hinit hF hnoq stim
  exact ⟨tr, h⟩

/-! ## `⊨₂` introduction and elimination -/

/-- Introduction rule — the raw hypothesis-form shape
(`run2 d σ fuel stim = .ok tr → …`, for every fuel), lifted in one step. -/
theorem Models2.of_run {d : Design2} {P : TraceProp2}
    (h : ∀ (σ : ScheduleOracle) (fuel : Nat) (stim tr : List SvState),
      run2 d σ fuel stim = .ok tr → P d stim tr) : d ⊨₂ P :=
  fun σ stim tr hr => hr.elim fun fuel hrun => h σ fuel stim tr hrun

/-- Introduction from the fuel-free run judgment (definitional; for
readability at use sites). -/
theorem Models2.of_runs {d : Design2} {P : TraceProp2}
    (h : ∀ (σ : ScheduleOracle) (stim tr : List SvState), Runs2 d σ stim tr → P d stim tr) :
    d ⊨₂ P :=
  h

/-- Elimination against a concrete interpreter run (any fuel). -/
theorem Models2.run {d : Design2} {P : TraceProp2} (h : d ⊨₂ P)
    {σ : ScheduleOracle} {fuel : Nat} {stim tr : List SvState}
    (hrun : run2 d σ fuel stim = .ok tr) : P d stim tr :=
  h σ stim tr ⟨fuel, hrun⟩

/-- Elimination against the fuel-free run judgment. -/
theorem Models2.runs {d : Design2} {P : TraceProp2} (h : d ⊨₂ P)
    {σ : ScheduleOracle} {stim tr : List SvState} (hr : Runs2 d σ stim tr) : P d stim tr :=
  h σ stim tr hr

/-- Monotonicity: weaken the property pointwise. -/
theorem Models2.imp {d : Design2} {P Q : TraceProp2}
    (hPQ : ∀ stim tr, P d stim tr → Q d stim tr) (h : d ⊨₂ P) : d ⊨₂ Q :=
  fun σ stim tr hr => hPQ stim tr (h σ stim tr hr)

/-- Conjunction: two `⊨₂` facts about one design combine pointwise.
(Its name hides the root `and` inside later `Models2.*` declarations. There,
write `&&` / `Bool.and`; see the module docstring.) -/
theorem Models2.and {d : Design2} {P Q : TraceProp2} (hP : d ⊨₂ P) (hQ : d ⊨₂ Q) :
    d ⊨₂ fun d stim tr => P d stim tr ∧ Q d stim tr :=
  fun σ stim tr hr => ⟨hP σ stim tr hr, hQ σ stim tr hr⟩

/-- `Models2.and` for `spec2`-lifted properties (stays in `spec2` form). -/
theorem Models2.and_spec2 {d : Design2} {P Q : List SvState → List SvState → Prop}
    (hP : d ⊨₂ spec2 P) (hQ : d ⊨₂ spec2 Q) :
    d ⊨₂ spec2 fun stim tr => P stim tr ∧ Q stim tr :=
  Models2.and hP hQ

/-- Every design satisfies the trivially true property. (Not named
`Models2.true`/`Models2.trivial`: those would hide the root `true`/`trivial`
inside every later `Models2.*` declaration.) -/
theorem Models2.spec2_true (d : Design2) : d ⊨₂ spec2 fun _ _ => True :=
  fun _ _ _ _ => trivial

/-- Every design satisfies the length law: one snapshot per stimulus entry. -/
theorem Models2.length (d : Design2) : d ⊨₂ spec2 fun stim tr => tr.length = stim.length :=
  fun _ _ _ hr => hr.length

/-- **Non-vacuity, packaged**: with `Terminates2`, a `⊨₂` property is
witnessed by an actual completed run on every schedule and stimulus. -/
theorem Models2.exists_of_terminates {d : Design2} {P : TraceProp2}
    (ht : Terminates2 d) (h : d ⊨₂ P) (σ : ScheduleOracle) (stim : List SvState) :
    ∃ tr, Runs2 d σ stim tr ∧ P d stim tr := by
  obtain ⟨tr, hr⟩ := ht σ stim
  exact ⟨tr, hr, h.runs hr⟩

/-! ## Examples: a one-gate design

`y = a & b` over 1-bit inputs. The `#guard` rows pin concrete `run2`
traces (the `x` startup values of undriven inputs flow through the gate,
held inputs keep their last driven value); the `example`s use the judgment
layer on it. -/

/-- `assign y = a & b;` with 1-bit ports. -/
private def and1D : Design2 :=
  { name := "and1"
    decls := #[
      { name := "a", width := 1, isInput := true },
      { name := "b", width := 1, isInput := true },
      { name := "y", width := 1, isOutput := true }]
    processes := #[.assign (.ident "y") (.binary .and (.ident "a") (.ident "b"))] }

private def and1Stim : List SvState := [
  [("a", LVec.lit "1"), ("b", LVec.lit "1")],
  [("b", LVec.lit "0")],
  [("b", LVec.lit "1")],
  [("a", LVec.lit "0")],
  []]

#guard (Res.toOption (run2 and1D σ_src 64 and1Stim)).map
        (fun tr => tr.map fun st => SvState.showSignal st "y")
      == some ["1", "0", "1", "0", "0"]

-- Same rows under the reversed schedule (one comb process: σ is irrelevant).
#guard (Res.toOption (run2 and1D σ_rev 64 and1Stim)).map
        (fun tr => tr.map fun st => SvState.showSignal st "y")
      == some ["1", "0", "1", "0", "0"]

-- An undriven input reads x at startup; `0 & x = 0`, `1 & x = x`.
#guard (Res.toOption (run2 and1D σ_src 64 [[("a", LVec.lit "0")], [("a", LVec.lit "1")]])).map
        (fun tr => tr.map fun st => SvState.showSignal st "y")
      == some ["0", "x"]

-- `⊨₂` from `Runs2.length`: the trace has one snapshot per stimulus entry.
example : and1D ⊨₂ spec2 fun stim tr => tr.length = stim.length :=
  Models2.of_run fun _ _ _ _ hrun => Runs2.length ⟨_, hrun⟩

-- A concrete completed run as a fuel-free `Runs2` fact (kernel-checked).
example : Runs2 and1D σ_src [[("a", LVec.lit "1"), ("b", LVec.lit "1")]]
    [[("a", LVec.lit "1"), ("b", LVec.lit "1"), ("y", LVec.lit "1")]] :=
  ⟨8, by decide +kernel⟩

-- Elimination: ANY `⊨₂` property of `and1D` holds of that concrete run.
example (P : TraceProp2) (h : and1D ⊨₂ P) :
    P and1D [[("a", LVec.lit "1"), ("b", LVec.lit "1")]]
      [[("a", LVec.lit "1"), ("b", LVec.lit "1"), ("y", LVec.lit "1")]] :=
  h.runs (σ := σ_src) ⟨8, by decide +kernel⟩

-- The snapshot-invariant eliminator, at its smallest instance (`I := True`):
-- the shape `Comb2.lean` instantiates with `StateShape` (`Runs2.comb_shape`).
example (σ : ScheduleOracle) (stim tr : List SvState) (h : Runs2 and1D σ stim tr) :
    ∀ st ∈ tr, True :=
  h.forall_mem_of_step (I := fun _ => True) trivial (fun _ _ _ _ _ _ _ _ => trivial)

-- Conjunction of two generic facts, staying in `spec2` form.
example : and1D ⊨₂ spec2 fun stim tr => tr.length = stim.length ∧ True :=
  Models2.and_spec2 (Models2.length _) (Models2.spec2_true _)

-- `Terminates2` through the invariant helper, on a process-free design (the
-- trivial invariant `True`; every settle is one no-op pass whose `==`
-- fixpoint check closes by `LawfulBEq`).
private def holdD : Design2 :=
  { name := "hold"
    decls := #[{ name := "a", width := 4, isInput := true }]
    processes := #[] }

private theorem holdD_terminates : Terminates2 holdD :=
  Terminates2.of_invariant (d := holdD) (fun _ => True) trivial (fun σ =>
    ⟨1, fun inputs st k _ => ⟨applyInputs2 holdD inputs st, k + 3, by
      simp [cycleStep2, combSettle2, combPass2, Design2.combIndices, Design2.resetIndices,
        Design2.edgeIndices, holdD, σ.choose_nil, edgePass2, commitNba2], trivial⟩⟩) (by rfl)

-- Non-vacuity, packaged: with `Terminates2`, a `⊨₂` fact is witnessed by an
-- actual run on every schedule and stimulus.
example (σ : ScheduleOracle) (stim : List SvState) :
    ∃ tr, Runs2 holdD σ stim tr ∧ tr.length = stim.length :=
  Models2.exists_of_terminates holdD_terminates (Models2.length holdD) σ stim

end LeanModels.Sv