tasks.arbiter.Spec
task statement
- Lines
- 171
- file
svgolf/tasks/arbiter/Spec.lean- sha256
21c83d684dee0be32115bad1167351a840b767d804188e62f0065aa55980bdfb- statements that rely on it
arbiter- download
tasks.arbiter.Spec.lean
svgolf/tasks/arbiter/Spec.leantasks.arbiter.Spec.lean
import SvGolf.TaskReact
/-!
# Task `arbiter`: a fair N-client arbiter
# (TRUSTED text, final, decided 2026-11-07; written 2026-11-05, txtask-v2's rules 2026-11-06; the reactive kind `ReactTask`, `SvGolf/TaskReact.lean`)
Module (port order as written; `N` is the module's parameter):
```systemverilog
module arbiter #(parameter int N = 8) (
input logic clk, rst,
input logic [N-1:0] req,
output logic [N-1:0] gnt
);
```
## What the statement says (`Statement sem`)
For every `N` with `1 ≤ N` and `N·N < 2^31`, the design `sem N`:
1. has exactly the ports above, and no two declarations of one name (`Interface`);
2. is clocked by `clk` alone with no asynchronous reset, never reads `clk` as data (`ClockedBy clk
none`), has no combinational path from an input port to an output port (`MooreOut`), and
terminates from every power-up state under every schedule and known stimulus (`TerminatesPU`);
3. has no construct that can create an `x` from known operands (`XSafe`, txtask-v2's rule 3);
4. from EVERY power-up state (every register and variable, 2-state ones included, starts at some
0/1 value, one value per signal), under every schedule, on every known stimulus, in every
completed run, at every RESET RELEASE `c1` (the entry `c1 - 1` asserts `rst`) and for every list
`x0 :: xs` of request vectors that the entries `c1 - 1, c1, c1 + 1, …` drive (the reset entry,
then the entries with `rst` = 0), the snapshots `c1 - 1, c1, c1 + 1, …` show known grant vectors
`y0, ys[0], ys[1], …` with
* **G0, reset**: `y0 = 0` (no grant comes out of reset);
* **G1, mutual exclusion**: every `ys[j]` has at most one bit set;
* **G2, no spurious grant**: a bit of `ys[j]` is set only if the same bit of `xs[j]` is;
* **G3, work conservation**: `xs[j] ≠ 0` implies `ys[j] ≠ 0`;
* **G4, bounded fairness** with `K = N - 1`: if requester `i` requests at all `N` entries
`j, …, j + N - 1` of the segment, then `ys[j + k]` grants `i` for some `k ≤ N - 1`.
The grant in snapshot `c` answers the request of entry `c` (the one the edge of cycle `c`
samples); by `MooreOut` it is what the requesters see on `gnt` for the whole next clock period,
while they drive entry `c + 1`. So a requester learns of its grant one period after it asserts its
request: a REGISTERED (Moore) arbiter, like verilog-axis `arbiter.v` (`grant_reg`), or a Mealy one
(OpenTitan `prim_arbiter_tree`) behind one output register.
## Decided 2026-11-05 (the study's defaults; reasons in `README.md`)
* Moore observation (`MooreOut`), from the kind.
* Safety (G1 to G3, per cycle: the ghost state is `Unit`) and bounded response (G4) with the bound
`K = N - 1` fixed here, not declared by the golfer: no design can beat it (`bound_tight`, among the
task's kernel-checked refutations, `tests/arbiter/negative/`), so a declared `K` would be a dead column.
* No environment assumption beyond reset and known inputs.
* "Any fair" policy: the statement pins fairness, not a grant order.
* Scored at `N = 8` (`task.json`).
## Why it cannot be satisfied vacuously or gamed (the kind's list, `SvGolf/TaskReact.lean`, plus)
* **Request-ignoring designs.** The free-running token ring that is SYNTCOMP-minimal for the
simple and round-robin arbiter families grants without requests (G2), the time-sliced ring
gated by the requests wastes slots (G3), grant-everyone breaks G1, never-grant breaks G3 and G4,
fixed priority starves (G4). The task's refutations (`tests/arbiter/negative/`) refute four of them
by kernel-evaluated runs.
* **The Mealy phantom.** Register `last <= req` and drive `gnt = (req == last) ? g : req` from a
correct registered grant `g`: every snapshot shows `g`, while in silicon `gnt = req` whenever
`req` changes, which breaks mutual exclusion. `MooreOut` rejects it (`mealy_refuted`).
* **Unknown and unreset values** (rules 2 and 3). The claims hold from every power-up state, so a
design may keep an unreset register only where its value does not matter. A correct round-robin
behind `if (~u)` on a never-reset, self-holding `u` fails G4 from the power-up `u = 0`
(`xopt_refuted`); behind `if (&u)` on a never-reset 2-state rotating ring `u`, from `u` all ones
(`ring_refuted`; ls-sv's `x` start would have made `u` 0 for ever); and with a flag set at the
first reset edge from the pre-reset `&u`, from `u` all ones (`reset_edge_refuted`). These runs
are ls-sv's: that the scored netlist agrees with them on defined values (obligation C) is
SAMPLED by the post-synthesis check, not proved. Yosys keeps such a register as a flop that
powers up to some value, and the checker refuses a never-loaded one (`u <= u`) where the pinned
flow removes its flip-flop while reads of it remain (it resolves each read separately).
* **Never-reset grant flops.** G0 requires a KNOWN zero at every reset snapshot, from every
power-up.
## What it does NOT pin, on purpose
* which requester wins (any policy meeting G4 is legal: round-robin pointer, mask, rotating
priority, matrix/LRU), and the state encoding (one-hot mask, binary pointer, matrix);
* the grant of a requester that drops its request before `N` consecutive requests: the reference
round-robin itself starves a requester that asks at every other entry (measured, `README.md`),
so a stronger form would reject every round-robin;
* anything while `rst` = 1 except the reset snapshot, and anything before the first reset.
-/
namespace SvGolf
open LeanModels.Sv
namespace Tasks.Arbiter
/-- At most one bit of `g` is set (one-hot or zero). -/
def AtMostOne {N : Nat} (g : BitVec N) : Prop :=
∀ i j, g.getLsbD i = true → g.getLsbD j = true → i = j
/-- **One cycle of arbitration**: `gnt` grants at most one requester (G1), only a requester of
`req` (G2), and someone whenever anyone requests (G3). Together: `gnt = 0` if `req = 0`, else `gnt`
is one bit of `req`. -/
def Grants {N : Nat} (req gnt : BitVec N) : Prop :=
AtMostOne gnt ∧ (∀ i, gnt.getLsbD i = true → req.getLsbD i = true) ∧ (req ≠ 0#N → gnt ≠ 0#N)
/-- **The task.** The ghost state is trivial (the safety property is per cycle); the obligations
are the requesters `i < N`: requesting triggers, being granted responds. -/
def task : ReactTask Nat where
inRange N := 1 ≤ N ∧ N * N < 2 ^ 31
clk := "clk"
rst := "rst"
inputs N := [("req", N)]
outputs N := [("gnt", N)]
atReset N := fun (g, ()) => g = 0#N
Ghost _ := Unit
ginit _ := ()
gnext _ _ _ _ := ()
safe _ _ := fun (r, ()) (g, ()) => Grants r g
Obl N := Fin N
trig _ i := fun (r, ()) => r.getLsbD i.val = true
resp _ i := fun (g, ()) => g.getLsbD i.val = true
/-- **The challenge statement**: the task at every `N` in range with the response bound `N - 1`
(G4: `N` consecutive requests are granted at least once). -/
abbrev Statement (sem : Nat → LeanModels.Sv.Design2) : Prop :=
task.Statement sem (fun N => N - 1)
/-! ## Sanity checks on the statement's data (proved; they add nothing to the trusted base) -/
/-- The scoring point `N = 8` and the probe points are in range; `N = 0` is not, nor any `N` with
`N·N ≥ 2^31` (a matrix arbiter declares `N·N` bits). -/
example : task.inRange 1 ∧ task.inRange 2 ∧ task.inRange 3 ∧ task.inRange 8 ∧
¬ task.inRange 0 ∧ ¬ task.inRange 46341 := by
simp [task]
/-- The port names are distinct. -/
example : ["clk", "rst", "req"].Nodup ∧ "gnt" ∉ ["clk", "rst", "req"] := by decide
/-- `Grants` with the bit indices bounded by `N` (bits at `i ≥ N` read `false`): decidable on
concrete vectors. -/
theorem grants_iff {N : Nat} {r g : BitVec N} : Grants r g ↔
(∀ i, i < N → ∀ j, j < N → g.getLsbD i = true → g.getLsbD j = true → i = j) ∧
(∀ i, i < N → g.getLsbD i = true → r.getLsbD i = true) ∧ (r ≠ 0#N → g ≠ 0#N) := by
constructor
· rintro ⟨h1, h2, h3⟩
exact ⟨fun i _ j _ hi hj => h1 i j hi hj, fun i _ hi => h2 i hi, h3⟩
· rintro ⟨h1, h2, h3⟩
exact ⟨fun i j hi hj => h1 i (BitVec.lt_of_getLsbD hi) j (BitVec.lt_of_getLsbD hj) hi hj,
fun i hi => h2 i (BitVec.lt_of_getLsbD hi) hi, h3⟩
/-- A one-hot grant of a requester is a legal arbitration; so is no grant without requests. -/
example : Grants (0b1011#4) (0b0010#4) ∧ Grants (0#4) (0#4) :=
⟨grants_iff.2 (by decide), grants_iff.2 (by decide)⟩
/-- The trivial answers are not legal arbitrations: grant everyone (G1), grant client 0 always
(G2), never grant (G3). -/
example : ¬ Grants (0b11#2) (0b11#2) ∧ ¬ Grants (0b00#2) (0b01#2) ∧ ¬ Grants (0b01#2) (0b00#2) :=
⟨fun h => absurd (grants_iff.1 h) (by decide), fun h => absurd (grants_iff.1 h) (by decide),
fun h => absurd (grants_iff.1 h) (by decide)⟩
/-- **Fixed priority starves** (G4 at `N = 2`, `K = 1`): both clients request twice in a row and
client 0 is granted both times, so the segment's guarantee fails for client 1. -/
example : ¬ task.Guar 2 1 [(0b11#2, ()), (0b11#2, ())] (0#2, ()) [(0b01#2, ()), (0b01#2, ())] := by
rintro ⟨-, -, hr⟩
obtain ⟨i, y, hi, hy, hg⟩ := hr (⟨1, by decide⟩ : Fin 2) 0 (by decide) (fun i x hi hx => by
have : i = 0 ∨ i = 1 := by omega
rcases this with rfl | rfl <;> (have hx' := Option.some.inj hx; subst hx'; exact rfl))
have : i = 0 ∨ i = 1 := by omega
rcases this with rfl | rfl <;> (have hy' := Option.some.inj hy; subst hy'; exact Bool.false_ne_true hg)
end Tasks.Arbiter
end SvGolf