Tasks / trusted base

SvGolf.Task

svGolf statement helpers

Lines
282
file
svgolf/SvGolf/Task.lean
sha256
b3f184fea55550ad95a76f0af490899f2d69f39dc5205d7408c69e1cf6de4e75
statements that rely on it
_example, add, aes_sbox, arbiter, clmul, fdivp, matmul, mul, sorter
download
SvGolf.Task.lean
svgolf/SvGolf/Task.leanSvGolf.Task.lean
import LeanModels.Sv.Spec2
import LeanModels.Sv.Comb2Proc

/-!
# svGolf task statements (TRUSTED)

This file is part of the **trusted base** of svGolf. Every challenge statement
is built from the definitions below, so a reader who wants to know what a
proved submission guarantees has to read this file, the task's own
`tasks/<name>/Spec.lean`, and the ls-sv semantics it rests on
(`LeanModels/Sv/Spec2.lean` for `Runs2`/`⊨₂`/`Terminates2`, `Sem2.lean` for
`run2`, `Comb2Base.lean` for `Acyclic` and `Comb2Proc.lean` for
`Design2.lowerComb`, the two `LoopFree` uses). Proof helpers live in
`SvGolf/Comb.lean` and are kernel-checked like any submission.

## What the statement covers today

The statement is about ls-sv's semantics of the design, and ls-sv resolves an
`x` ONE way, by IEEE 1800's simulation rules: an `if` whose condition is `x`
takes its else branch (§12.4), and a plain `case` compares with `===`
(§12.5), so a subject with `x` bits matches no item of known bits. svGolf's rule is that an
`x` is an unknown 0/1 value and a valid design is correct for EVERY resolution
of it. Until ls-sv's every-resolution semantics lands, the statement does not
say that: a design in which an `x` (an undriven signal, an out-of-range
select, a constant with unknown bits) reaches a decision can satisfy it while
another resolution, and the synthesized netlist, compute something else
(`tests/_example/attacks/undriven_mux.sv`, `oob_rare.sv`). The checker's
x-source rules (`score/srcgate.py`, `score/banlist.py`, `score/fourstate.py`)
refuse such designs, but they are untrusted Python and decide at the
parameter points `check.py` instantiates, not at every `N` the statement
covers. A submission's validity by svGolf's rules therefore also rests on those
stages, until the statement quantifies over every resolution.

`sem N` is ls-sv's instantiation at the parameter value `N`. It evaluates `int`
constants on ls-sv's unbounded constant tier, where a simulator evaluates them
in signed 32-bit arithmetic. Every family task bounds its range so that every
width and every `int` localparam it implies stays below `2^31` (`2N` in `mul`,
`W + P + 1` in `fdivp`; decided 2026-11-06), where the two agree, so the
statement covers no parameter value at which an `int` expression of the
interface wraps. An `int` expression the interface does not imply (an internal
`N * N`) can still wrap at a value in range: ls-sv's loader refuses that at the
defaults (`PDesign.int32Agrees`), and the checker at the points it probes only
(svgolf/README.md, "Checker v0 limits").

## What a combinational task says

A task fixes, for every value `N` of the design's one parameter in the
task's range (`CombTask.inRange`):

* an **interface**: the input ports (name, width) and the output ports
  (name, width), each list in port order (`CombTask.inputs`,
  `CombTask.outputs`; widths are functions of `N`);
* a **function** from the input values to the output values, on `BitVec`s
  of exactly those widths (`CombTask.fn`).

`CombTask.Statement t sem` says that the design family `sem : Nat → Design2`
(the `name.sem` constant that `load_design_sv2` defines from the player's
envelope) satisfies, at every `N` with `t.inRange N`, the three conjuncts of
`CombTask.SatisfiedAt`:

1. **`Interface`**: the design's input ports, in declaration order with their
   declared widths, are EXACTLY `t.inputs N`, and its output ports are EXACTLY
   `t.outputs N`.
2. **`Unclocked`, `LoopFree` and `Terminates2`**: no process of the design is
   clocked (no `always_ff`, no `always @(posedge …)`, no async-reset block:
   ls-sv's edge phase is empty), the design has no combinational loop (it
   lowers to continuous assigns that are ranked: each reads only what
   lower-ranked ones write), and under every schedule and every stimulus, the
   run completes (`Spec2.lean`). The three are one conjunct, so that
   conjuncts 1 and 3 keep their positions (`h.1`, `h.2.2`).
3. **`Computes`**: in every completed run, under every schedule and every
   stimulus, every cycle whose stimulus entry drives every input port with a
   known value (a `BitVec` at the port's width) ends in a snapshot whose output
   ports hold exactly the known values `t.fn N` assigns.

## Why it cannot be satisfied vacuously

The statement was written against the following ways a design could make a
weaker statement true without computing the function. Each is ruled out by
one of the conjuncts; the task's `tests/<name>/negative/` directory holds designs that try
them, with kernel-checked refutations.

* **Never settling, or having no run.** `⊨₂` (`Models2`) quantifies over
  *completed* runs only, so a design that never settles satisfies every `⊨₂`
  property. `Terminates2` requires a completed run for every schedule and
  every stimulus, so with conjunct 2 the property in conjunct 3 is about runs
  that exist. A design the loader refused (`errorDesign`: every run is
  `.unsupported`) has no completed run, so it fails conjunct 2.
* **Hypotheses that never hold.** Conjunct 3's hypothesis is about the
  STIMULUS, which the statement quantifies over, not about the design's
  state. For every input valuation `v` there is a stimulus entry binding
  exactly `v` (when the port names are distinct; `SvGolf/Comb.lean` proves
  this non-vacuity fact, `CombTask.SatisfiedAt.exists_run`), and by
  conjunct 2 it has a completed run whose snapshot must carry `t.fn N v`.
  A snapshot-form hypothesis ("whenever the input ports hold known values in
  the snapshot") would be weaker: a design that drives its own input ports
  could keep that hypothesis false forever. The stimulus form is immune.
* **Stimulus conventions.** `applyInputs2` stores a stimulus value verbatim
  at its own width and silently ignores names that are not input ports
  (`Spec2.lean`, "Stimulus conventions"). The hypothesis therefore uses the
  task's port names and `BitVec`s at the task's widths, and conjunct 1 pins
  that those names ARE the design's input ports with exactly those widths. A
  design that declared `a` with another width, under another name, or as an
  internal signal would either fail conjunct 1 or see different values than
  the hypothesis describes.
* **Extra or missing ports.** Conjunct 1 is list equality, not inclusion: an
  extra input (which the stimulus would never drive, so it reads `x`, or
  which a design could use as a hidden "mode" pin), an extra output, a
  missing port, a port with another direction, and an `inout` port (it would
  appear in both lists) are all rejected.
* **Unknown values.** Outputs must hold `LVec.ofBitVec x`, a fully known
  4-state vector of exactly the output's width. A design that leaves an output
  undriven, drives the output from some other variable and leaves the port
  itself at `x`, or lets an `x` reach an output bit for some known input (an
  undriven `z` in `y = a & b & ~z`, `tests/_example/negative/reads_x.sv`),
  fails conjunct 3. An `x` that reaches only a decision does not: see "What
  the statement covers today" above.
* **A combinational loop.** A run starts every variable at `x` (or its
  initializer) and settles (`initState2`, `run2`), so a loop is read at the
  one state the settle reaches from `x`, while a circuit powers up in any
  0/1 state the loop can hold and keeps it
  (`always_comb u = t; always_comb if (u) t = 1'b1; else t = 1'b0;` settles
  at `t = 0` and holds `t = 1` as well). The statement does not quantify over
  those states; `LoopFree` rejects every design with a loop instead, and with
  it a latch (`if (en) q = d;`: the lowering refuses a body that keeps a
  value it writes), whose power-up value is the same question. It is the
  certificate the proof helpers already use (`SvGolf/Comb.lean`). Its rank
  is on PROCESSES, not bits, so it rejects more than loops: one assign that
  reads a window it writes, or two processes that read each other's windows,
  are rejected even when no bit depends on itself (write such bits as
  separate assigns), and so is an `always_comb` body outside the lowering's
  profile (`Comb2Proc.lean`, "Gaps").
* **History.** Conjunct 3 must hold at EVERY cycle of EVERY run, whatever the
  earlier stimulus entries were, so a design whose outputs depend on earlier
  cycles (a register, a toggling mode) fails it.
* **A clock that never ticks.** ls-sv runs every edge-phase process once per
  cycle WHATEVER its clock signal holds (`cycleStep2`: `Design2.edgeIndices`
  filters on the process kind only; the charter's single-clock gap), then
  commits the nonblocking assignments and settles. So
  `logic clk; always_ff @(posedge clk) y <= f;` with `clk` undriven (or tied
  to `1'b0`) holds the right `y` in every ls-sv snapshot, while every
  simulator and Yosys see a clock that never rises and leave `y` at `x` (Yosys
  then scores the design at 0 GE). The `Unclocked` half of conjunct 2 rejects
  every clocked process, which a combinational task never needs. It goes
  when ls-sv's sequential layer models clock edges.
* **Held inputs.** Conjunct 3 constrains only cycles whose stimulus entry
  re-drives every input. A cycle that holds an input instead starts from the
  same state: `applyInputs2` writes with `SvState.set`, which replaces a
  binding in place, so re-driving the value an input already holds leaves the
  state unchanged. So no design can tell the two cases apart.

What the statement does NOT pin, on purpose: signedness and the two-state/four-state choice
of the port declarations (they do not change the bits a port carries),
internal signals, and the parameter's name and declared type. The checker
(`check.py`) separately pins the top module name and the parameter's name,
since the scoring script overrides the parameter by name.

## Conventions

* `Sig = String × Nat` is a port signature: name and width in bits.
* `Vals [w₁, …, wₖ]` is `BitVec w₁ × (… × (BitVec wₖ × Unit))`: one known
  value per port, in port order. A task's function is written with tuple
  patterns, e.g. `fn := fun _ (a, b, ()) => (a &&& b, ())`.
* `HoldAll st sigs v`: in state `st`, each port of `sigs` is bound to the
  known value `v` gives it (`SvState.lookup`, first binding wins, as
  everywhere in ls-sv).
-/

namespace SvGolf

open LeanModels.Sv

/-! ## Ports -/

/-- A port signature: the port's name and its width in bits. -/
abbrev Sig := String × Nat

/-- The input ports of `d` with their declared widths, in declaration order.
`instantiate2` lists the ports first, in port-list order. A declaration is
an input port iff its `isInput` flag is set. -/
def inputPorts (d : Design2) : List Sig :=
  d.decls.toList.filterMap fun dc => if dc.isInput then some (dc.name, dc.width) else none

/-- The output ports of `d` with their declared widths, in declaration order.
A declaration is an output port iff its `isOutput` flag is set. -/
def outputPorts (d : Design2) : List Sig :=
  d.decls.toList.filterMap fun dc => if dc.isOutput then some (dc.name, dc.width) else none

/-! ## Known values -/

/-- One known value per port: `Vals [w₁, …, wₖ] = BitVec w₁ × (… × (BitVec wₖ × Unit))`.
Reducible, so that tuple patterns `(a, b, ())` elaborate against it. -/
@[reducible] def Vals : List Nat → Type
  | [] => Unit
  | w :: ws => BitVec w × Vals ws

/-- `HoldAll st sigs v`: in state `st`, every port `(n, w)` of `sigs` is bound
to the fully known `w`-bit vector that `v` assigns it. -/
def HoldAll (st : SvState) : (sigs : List Sig) → Vals (sigs.map Prod.snd) → Prop
  | [], _ => True
  | (n, _) :: sigs, v => SvState.lookup st n = some (LVec.ofBitVec v.1) ∧ HoldAll st sigs v.2

/-! ## No clocked process -/

/-- No process of `d` is clocked: none is an `always_ff`, a plain
`always @(posedge …)` or an async-reset block, the processes ls-sv runs in
its edge phase (`Process2.isEdgePhase`). What remains are continuous assigns
and `always_comb` blocks (and loud `unsupported` nodes, which never complete
a run). -/
def Unclocked (d : Design2) : Prop :=
  ∀ p ∈ d.processes.toList, p.isEdgePhase = false

/-! ## No combinational loop -/

/-- No combinational loop: `d` LOWERS to continuous assigns
(`Design2.lowerComb`, `LeanModels/Sv/Comb2Proc.lean`: a continuous assign is
kept and an `always_comb` body becomes ONE assign by symbolic execution; any
other process, and a body that reads a window it writes, latches included,
refuse) and the lowered design is acyclic (`Acyclic`,
`LeanModels/Sv/Comb2Base.lean`: a rank on its processes such that each
process reads only windows written by strictly lower-ranked processes). -/
def LoopFree (d : Design2) : Prop :=
  ∃ d', d.lowerComb = some d' ∧ Acyclic d'

/-! ## Combinational tasks -/

/-- A combinational svGolf task for a one-parameter design family: the
parameter range, the interface at each parameter value, and the function the
outputs must compute. -/
structure CombTask where
  /-- The parameter values the statement covers ("for every `N` in range"). A
  family task bounds them so that every width and `int` localparam they imply
  stays below `2^31`, the values an `int` holds (`int` is 32-bit signed,
  IEEE 1800 §6.11): `N < 2^31` where the widths are `N`, `N < 2^30` where a
  width is `2N`. -/
  inRange : Nat → Prop
  /-- The input ports at parameter `N`: (name, width), in port order. -/
  inputs : Nat → List Sig
  /-- The output ports at parameter `N`: (name, width), in port order. -/
  outputs : Nat → List Sig
  /-- The function: known input values ↦ known output values. -/
  fn : (N : Nat) → Vals ((inputs N).map Prod.snd) → Vals ((outputs N).map Prod.snd)

namespace CombTask

variable (t : CombTask)

/-- Conjunct 1: the design's input and output ports are exactly the task's,
names, order and widths included. -/
def Interface (N : Nat) (d : Design2) : Prop :=
  inputPorts d = t.inputs N ∧ outputPorts d = t.outputs N

/-- Conjunct 3: in every completed run (every schedule, every stimulus),
every cycle `c` whose stimulus entry drives each input port with a known
value of the port's width ends in a snapshot whose output ports hold exactly
`t.fn N` of those values. -/
def Computes (N : Nat) (d : Design2) : Prop :=
  d ⊨₂ spec2 fun stim tr =>
    ∀ (c : Nat) (inputs st : SvState), stim[c]? = some inputs → tr[c]? = some st →
      ∀ v, HoldAll inputs (t.inputs N) v → HoldAll st (t.outputs N) (t.fn N v)

/-- The statement at one parameter value: interface; no clocked process, no
combinational loop and termination; function. Conjunct 2 is the triple
`Unclocked d ∧ LoopFree d ∧ Terminates2 d`. -/
def SatisfiedAt (N : Nat) (d : Design2) : Prop :=
  t.Interface N d ∧ (Unclocked d ∧ LoopFree d ∧ Terminates2 d) ∧ t.Computes N d

/-- **The challenge statement** for a one-parameter design family
`sem : Nat → Design2` (a `load_design_sv2` constant `name.sem`): the design
satisfies the task at every parameter value in range. -/
def Statement (sem : Nat → Design2) : Prop :=
  ∀ N, t.inRange N → t.SatisfiedAt N (sem N)

/-- The challenge statement for a parameterless design (`name.sem : Design2`,
a fixed-size task): the task at parameter value `0`, whose interface and
function a fixed-size task writes without using `N`. -/
def StatementFixed (d : Design2) : Prop :=
  t.SatisfiedAt 0 d

end CombTask

end SvGolf