Golf scores on ASAP7, a predictive 7 nm library: never fabricated, and like sky130's score it counts no wires. sky130, a real 130 nm process, is the default. How ASAP7 is scored, and its quirks.

Tasks / mul

N-bit multiplier mul

The full 2N-bit product of two unsigned N-bit numbers, with logic gates only.

open for submissions parametric scored at N = 16

Par
51 311µm²·nsmul_tree_h · 8563.2 µm² × 5.99 ns
Record
42 530µm²·nslate-carry · 7961.4 µm² × 5.34 ns
Under par
−17%
Par
148.4µm²·nsmul_tree_h · 127.954 µm² × 1.16 ns
Record
131.5µm²·nslate-carry · 120.854 µm² × 1.09 ns
Under par
−11%

Score

ASAP7 · area × time · µm²·ns · lower is better
mul: the score of each entry by date6 entries by date (x) and score in µm²·ns (y); the line is the best score so far, the dashed line par, and 3 unproved baselines are labelled on the right. The table under the chart lists the same numbers.40k50k60k70k80k90k26 Oct2 Nov9 Novdate enteredscore (µm²·ns) · lower is betteryosys_mul · baseline, not proved · banned operator · Score: 55 259 µm²·ns · Area: 8826.0 µm² · Delay: 6261 psdadda_ks_16 · baseline, not proved · Score: 46 009 µm²·ns · Area: 8937.3 µm² · Delay: 5148 psdadda_rc_16 · baseline, not proved · Score: 44 946 µm²·ns · Area: 8230.4 µm² · Delay: 5461 pspar: mul_tree_h, 51 311 µm²·nsyosys_mul55 259 · banned opPAR51 311 · proveddadda_ks_1646 009 · not proveddadda_rc_1644 946 · not provedthe best score so farmul_wallace · proved reference · 2026-10-30T05:26:59Z · Score: 53 635 µm²·ns · Area: 8966.1 µm² · Delay: 5982 psmul_n · proved reference · 2026-10-29T10:46:16Z · Score: 90 424 µm²·ns · Area: 8290.5 µm² · Delay: 10 907 psmul_tree · proved reference · 2026-10-30T03:37:11Z · Score: 52 729 µm²·ns · Area: 8603.3 µm² · Delay: 6129 psmul_tree_h · proved reference · par · 2026-10-30T12:20:05Z · Score: 51 311 µm²·ns · Area: 8563.2 µm² · Delay: 5992 psplus-minus-two · submission · 2026-11-05 · Score: 50 498 µm²·ns · Area: 7978.9 µm² · Delay: 6329 pslate-carry · submission · 2026-11-05 · Score: 42 530 µm²·ns · Area: 7961.4 µm² · Delay: 5342 ps
mul: the score of each entry by date6 entries by date (x) and score in µm²·ns (y); the line is the best score so far, the dashed line par, and 3 unproved baselines are labelled on the right. The table under the chart lists the same numbers.40k60k80k26 Oct2 Nov9 Novdate enteredscore (µm²·ns) · lower is betteryosys_mul · baseline, not proved · banned operator · Score: 55 259 µm²·ns · Area: 8826.0 µm² · Delay: 6261 psdadda_ks_16 · baseline, not proved · Score: 46 009 µm²·ns · Area: 8937.3 µm² · Delay: 5148 psdadda_rc_16 · baseline, not proved · Score: 44 946 µm²·ns · Area: 8230.4 µm² · Delay: 5461 pspar: mul_tree_h, 51 311 µm²·nsPARthe best score so farmul_wallace · proved reference · 2026-10-30T05:26:59Z · Score: 53 635 µm²·ns · Area: 8966.1 µm² · Delay: 5982 psmul_n · proved reference · 2026-10-29T10:46:16Z · Score: 90 424 µm²·ns · Area: 8290.5 µm² · Delay: 10 907 psmul_tree · proved reference · 2026-10-30T03:37:11Z · Score: 52 729 µm²·ns · Area: 8603.3 µm² · Delay: 6129 psmul_tree_h · proved reference · par · 2026-10-30T12:20:05Z · Score: 51 311 µm²·ns · Area: 8563.2 µm² · Delay: 5992 psplus-minus-two · submission · 2026-11-05 · Score: 50 498 µm²·ns · Area: 7978.9 µm² · Delay: 6329 pslate-carry · submission · 2026-11-05 · Score: 42 530 µm²·ns · Area: 7961.4 µm² · Delay: 5342 ps
  • submission
  • proved reference
  • the best score so far
  • par
  • a baseline's score (not proved)
Score = area × time: the sky130 cell area of the scored netlist in µm² times the critical-path delay in ns, from the pinned flow, so µm²·ns. Lower is better. It is the one ranked number: Par (the best proved reference's score, the bar to beat), Record (the best accepted score so far, par while no submission beats it), Under par (how far the record is below par, as a share of par's score) and the leaderboard use it. The Pareto front, area in GE (NAND2 gate equivalents), depth and the other numbers are for exploring, under compare the designs; reproduce a score says how to get the same numbers. Rated with Yosys 0.53 (git 53c22ab7, the Homebrew build for macOS arm64, clang++ 17, -O3) and its yosys-abc (ABC 1.01 of 6 May 2025): the flow is deterministic on that build (measured on 52 designs). Another build of the same version can break ABC's ties differently (measured: 3 fdivp designs, up to 2.9% in delay), so a number made on another platform may differ slightly: the build and its hashes. Dots are the entries by date; the proved references are the first entries, dated by when they joined the task. The dotted levels are baselines: scored, never proved.
every entry by date
datedesignkindscore (µm²·ns)area × time
2026-10-29mul_nproved reference90 4248290.5 µm² × 10.9 ns
2026-10-30mul_treeproved reference52 7298603.3 µm² × 6.13 ns
2026-10-30mul_wallaceproved reference53 6358966.1 µm² × 5.98 ns
2026-10-30mul_tree_hproved reference · par51 3118563.2 µm² × 5.99 ns
2026-11-05late-carrysubmission42 5307961.4 µm² × 5.34 ns
2026-11-05plus-minus-twosubmission50 4987978.9 µm² × 6.33 ns
–dadda_rc_16baseline, not proved44 9468230.4 µm² × 5.46 ns
–dadda_ks_16baseline, not proved46 0098937.3 µm² × 5.15 ns
–yosys_mulbaseline, not proved · banned operator55 2598826.0 µm² × 6.26 ns
mul: the score of each entry by date6 entries by date (x) and score in µm²·ns (y); the line is the best score so far, the dashed line par, and 3 unproved baselines are labelled on the right. The table under the chart lists the same numbers.12515017520022525026 Oct2 Nov9 Novdate enteredscore (µm²·ns) · lower is betteryosys_mul · baseline, not proved · banned operator · Score: 150.1 µm²·ns · Area: 131.789 µm² · Delay: 1139 psdadda_rc_16 · baseline, not proved · Score: 135.7 µm²·ns · Area: 123.391 µm² · Delay: 1100 psdadda_ks_16 · baseline, not proved · Score: 120.1 µm²·ns · Area: 133.174 µm² · Delay: 902 pspar: mul_tree_h, 148.4 µm²·nsyosys_mul150.1 · banned opPAR148.4 · proveddadda_rc_16135.7 · not proveddadda_ks_16120.1 · not provedthe best score so farmul_wallace · proved reference · 2026-10-30T05:26:59Z · Score: 158.8 µm²·ns · Area: 133.917 µm² · Delay: 1186 psmul_n · proved reference · 2026-10-29T10:46:16Z · Score: 245.5 µm²·ns · Area: 123.930 µm² · Delay: 1981 psmul_tree · proved reference · 2026-10-30T03:37:11Z · Score: 153.1 µm²·ns · Area: 128.552 µm² · Delay: 1191 psmul_tree_h · proved reference · par · 2026-10-30T12:20:05Z · Score: 148.4 µm²·ns · Area: 127.954 µm² · Delay: 1160 psplus-minus-two · submission · 2026-11-05 · Score: 156.5 µm²·ns · Area: 119.746 µm² · Delay: 1307 pslate-carry · submission · 2026-11-05 · Score: 131.5 µm²·ns · Area: 120.854 µm² · Delay: 1088 ps
mul: the score of each entry by date6 entries by date (x) and score in µm²·ns (y); the line is the best score so far, the dashed line par, and 3 unproved baselines are labelled on the right. The table under the chart lists the same numbers.15020025026 Oct2 Nov9 Novdate enteredscore (µm²·ns) · lower is betteryosys_mul · baseline, not proved · banned operator · Score: 150.1 µm²·ns · Area: 131.789 µm² · Delay: 1139 psdadda_rc_16 · baseline, not proved · Score: 135.7 µm²·ns · Area: 123.391 µm² · Delay: 1100 psdadda_ks_16 · baseline, not proved · Score: 120.1 µm²·ns · Area: 133.174 µm² · Delay: 902 pspar: mul_tree_h, 148.4 µm²·nsPARthe best score so farmul_wallace · proved reference · 2026-10-30T05:26:59Z · Score: 158.8 µm²·ns · Area: 133.917 µm² · Delay: 1186 psmul_n · proved reference · 2026-10-29T10:46:16Z · Score: 245.5 µm²·ns · Area: 123.930 µm² · Delay: 1981 psmul_tree · proved reference · 2026-10-30T03:37:11Z · Score: 153.1 µm²·ns · Area: 128.552 µm² · Delay: 1191 psmul_tree_h · proved reference · par · 2026-10-30T12:20:05Z · Score: 148.4 µm²·ns · Area: 127.954 µm² · Delay: 1160 psplus-minus-two · submission · 2026-11-05 · Score: 156.5 µm²·ns · Area: 119.746 µm² · Delay: 1307 pslate-carry · submission · 2026-11-05 · Score: 131.5 µm²·ns · Area: 120.854 µm² · Delay: 1088 ps
  • submission
  • proved reference
  • the best score so far
  • par
  • a baseline's score (not proved)
Score = area × time: the ASAP7 cell area of the scored netlist in µm² times the critical-path delay in ns, from the pinned flow, so µm²·ns. Lower is better. In this view Par (the best proved reference's score, the bar to beat), Record (the best accepted score so far, par while no submission beats it), Under par (how far the record is below par, as a share of par's score) and the leaderboard use it; the records' ranked score is sky130's, the default view. The Pareto front, area in GE (NAND2 gate equivalents), depth and the other numbers are for exploring, under compare the designs; reproduce a score says how to get the same numbers. ASAP7 view. These numbers are on ASAP7 (asap7sc7p5t), a predictive 7 nm library made for research: it has never been fabricated, so no chip can confirm them. Like sky130's score they count no wires. sky130, a real 130 nm process, is the default view (how ASAP7 is scored, and its quirks). Rated with Yosys 0.53 (git 53c22ab7, the Homebrew build for macOS arm64, clang++ 17, -O3) and its yosys-abc (ABC 1.01 of 6 May 2025): the flow is deterministic on that build (measured on 52 designs). Another build of the same version can break ABC's ties differently (measured: 3 fdivp designs, up to 2.9% in delay), so a number made on another platform may differ slightly: the build and its hashes. Dots are the entries by date; the proved references are the first entries, dated by when they joined the task. The dotted levels are baselines: scored, never proved.
every entry by date
datedesignkindscore (µm²·ns)area × time
2026-10-29mul_nproved reference245.5123.930 µm² × 1.98 ns
2026-10-30mul_treeproved reference153.1128.552 µm² × 1.19 ns
2026-10-30mul_wallaceproved reference158.8133.917 µm² × 1.19 ns
2026-10-30mul_tree_hproved reference · par148.4127.954 µm² × 1.16 ns
2026-11-05late-carrysubmission131.5120.854 µm² × 1.09 ns
2026-11-05plus-minus-twosubmission156.5119.746 µm² × 1.31 ns
–dadda_rc_16baseline, not proved135.7123.391 µm² × 1.10 ns
–dadda_ks_16baseline, not proved120.1133.174 µm² × 0.902 ns
–yosys_mulbaseline, not proved · banned operator150.1131.789 µm² × 1.14 ns

This text quotes sky130 numbers and compares the designs on sky130, the default library; the score, the boards and the charts of this page are on ASAP7, where the order of the designs can differ.

Multiply two unsigned N-bit numbers: p = a * b, all 2N bits of the product, for every N ≥ 1, scored at N = 16.

Run-time + - * / % ** and the ordering comparisons < <= > >= are banned, so the multiplier is built from partial products and adders made of gates. The proved array multiplier has 8290.5 µm² and a 10.9 ns critical path on sky130 (1962.576 GE, depth 88). The proved tree references are much faster for a little more area: par is the tree mul_tree_h, 8563.2 µm² at 5.99 ns, 51 311 µm²·ns. An unproved Dadda tree with a ripple final adder is both smaller and faster than par, 8230.4 µm² at 5.46 ns (44 946 µm²·ns): proving a tree like it is the open challenge.

Interface #

Write a module with exactly this header: port names, directions, widths and order matter, and so do the module's name and its parameters.

mul_n.sv
module mul_n #(parameter int N = 16) (
  input  logic [N-1:0]   a, b,
  output logic [2*N-1:0] p
);
portdirectionwidth (bits)
ainputN
binputN
poutput2*N

The trusted statement #

A proof must establish the task's trusted statement, the constant SvGolf.Tasks.Mul.Statement of Spec.lean (module tasks.mul.Spec). Below it is split into the properties a circuit must have, one card each: what the property guarantees in plain words, and its exact Lean text. Every proved reference has all of them. The last tab, what does not count, lists the designs the checker rejects.

Properties

A design meets the trusted statement exactly when, for every width N with 1 ≤ N < 2^30, it has all of the properties below. Lean checks that, together, they say exactly what the statement says, nothing less and nothing more (the theorem statement_iff_properties).

PARAMS_IN_RANGE is the range the statement covers, not a property: every other card must hold at each value in it.

PARAMS_IN_RANGErangeThe widths the statement covers: every N with 1 ≤ N < 2^30 (so that the output width 2N, an int expression of the module, stays below 2^31, the values an int holds).

The statement covers every width N with 1 ≤ N < 2^30 at once (the module's port widths are int expressions of N, such as 2N, and an int holds no value of 2^31 or more): one proof works for all of them, not just for the width the design is scored at (N = 16). Every property below must hold at each width.

ParamsInRange
/-- The widths the statement covers: every `N` with `1 ≤ N < 2^30` (so that the output width `2N`,
an `int` expression of the module, stays below `2^31`, the values an `int` holds). -/
def ParamsInRange (N : Nat) : Prop :=
  1 ≤ N ∧ N < 2 ^ 30
svgolf/tasks/mul/Properties.lean:17Open in code
INTERFACEExactly these ports, in this order, with these widths.

The module has exactly the input ports a and b (N bits each) and exactly the output port p (2N bits), with these widths, in this order. Nothing is missing and nothing is extra: an extra input could serve as a hidden mode pin, and the checker and the scoring tools drive and read the design by these names and widths.

Interface
/-- Exactly these ports, in this order, with these widths. -/
def Interface (N : Nat) (d : Design2) : Prop :=
  inputPorts d = [("a", N), ("b", N)] ∧ outputPorts d = [("p", 2 * N)]
svgolf/tasks/mul/Properties.lean:22Open in code
UNCLOCKEDNo process runs at a clock edge (no always_ff, no always @(posedge …)).

No process of the design runs at a clock edge: no always_ff, no always @(posedge …), no asynchronous-reset block; only continuous assignments and always_comb blocks. The task is combinational, so it never needs one. The rule closes a gap between Lean's model of SystemVerilog, which the proof is about, and real tools: the model runs every clocked block once per cycle whatever its clock does, so a design that registers correct outputs on a clock nothing drives would look right there, while a simulator sees a clock that never ticks and synthesis removes the registers.

Unclocked
/-- No process runs at a clock edge (no `always_ff`, no `always @(posedge …)`). -/
def Unclocked (d : Design2) : Prop :=
  ∀ p ∈ d.processes.toList, p.isEdgePhase = false
svgolf/tasks/mul/Properties.lean:26Open in code
LOOP_FREENo combinational loop: the design lowers to continuous assigns (Design2.lowerComb) that are ranked (Acyclic): each reads only what lower-ranked ones write.

The design has no combinational loop: written as continuous assignments (each always_comb block becomes one), its processes can be ranked so that each reads only what lower-ranked ones write. A latch (an always_comb block that keeps a value on some path) is refused with it. Lean's model starts every signal at x and settles, so it would read a loop at one state, while a circuit powers up in any 0/1 state the loop can hold and keeps it; the rule removes that question instead of answering it. The ranking is of processes, not bits, so a process that reads a part of a signal it writes itself is refused too, even when no bit depends on itself: write such bits as separate assignments.

LoopFree
/-- No combinational loop: the design lowers to continuous assigns (`Design2.lowerComb`) that are
ranked (`Acyclic`): each reads only what lower-ranked ones write. -/
def LoopFree (d : Design2) : Prop :=
  ∃ d', d.lowerComb = some d' ∧ Acyclic d'
svgolf/tasks/mul/Properties.lean:30Open in code
TERMINATESUnder every schedule and every stimulus, the run completes.

Whatever order the simulator picks for processes that are ready at the same moment (the schedule), and whatever values the inputs take, simulating the design completes: every cycle settles. A combinational loop that oscillates fails this, and so does a design the tools could not read. It is also what gives the next property its teeth: that one is about completed runs, and a design with no completed run would satisfy it without computing anything.

Terminates
/-- Under every schedule and every stimulus, the run completes. -/
def Terminates (d : Design2) : Prop :=
  ∀ (σ : ScheduleOracle) (stim : List SvState), ∃ tr, Runs2 d σ stim tr
svgolf/tasks/mul/Properties.lean:35Open in code
COMPUTES_PRODUCTIn every completed run, every cycle whose stimulus drives a and b with known values ends with p holding their full product, on 2N bits.

In every cycle in which the inputs a and b are driven with known 0/1 values, the output p settles to a × b, the product of the two unsigned numbers. p has 2N bits, enough for the largest product, so nothing is truncated. The outputs must be fully known 0/1 values in every cycle of every run, whatever the earlier cycles were, so they cannot depend on history.

ComputesProduct
/-- In every completed run, every cycle whose stimulus drives `a` and `b` with known values ends
with `p` holding their full product, on `2N` bits. -/
def ComputesProduct (N : Nat) (d : Design2) : Prop :=
  d ⊨₂ spec2 fun stim tr =>
    ∀ (c : Nat) (inputs st : SvState), stim[c]? = some inputs → tr[c]? = some st →
      ∀ (a b : BitVec N),
        HoldAll inputs [("a", N), ("b", N)] (a, b, ()) →
        HoldAll st [("p", 2 * N)] (BitVec.ofNat (2 * N) (a.toNat * b.toNat), ())
svgolf/tasks/mul/Properties.lean:39Open in code

Definitions

The names the properties use, each with its Lean text. Trusted text is what a proof is about; a helper of Properties.lean is not trusted: Lean checks it as part of statement_iff_properties.

This task's trusted text

StatementSpec.leanThe trusted challenge statement that these properties restate, part by part.
Statement
/-- **The challenge statement.** A submission proves
`SvGolf.Tasks.Mul.Statement Submission.design.sem`. -/
abbrev Statement : (Nat → LeanModels.Sv.Design2) → Prop := task.Statement
svgolf/tasks/mul/Spec.lean:43Open in code
taskSpec.leanThe task as data: the parameter range, the ports at each parameter value, and the function the outputs must compute.
task
/-- The task: inputs `a b : BitVec N`, output `p = a * b` on `2N` bits, for
`1 ≤ N < 2^30`. -/
def task : CombTask where
  inRange N := 1 ≤ N ∧ N < 2 ^ 30
  inputs N := [("a", N), ("b", N)]
  outputs N := [("p", 2 * N)]
  fn N := fun (a, b, ()) => (BitVec.ofNat (2 * N) (a.toNat * b.toNat), ())
svgolf/tasks/mul/Spec.lean:35Open in code

The shared trusted base

CombTask.SatisfiedAtTask.leanThe trusted form of a combinational statement at one parameter value: the interface, then no clock and termination, then the function.
SatisfiedAt
/-- 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
svgolf/SvGolf/Task.lean:262Open in code
inputPortsTask.leanThe design's input ports, each with its declared width, in declaration order.
inputPorts
/-- 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
svgolf/SvGolf/Task.lean:178Open in code
outputPortsTask.leanThe design's output ports, each with its declared width, in declaration order.
outputPorts
/-- 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
svgolf/SvGolf/Task.lean:184Open in code
HoldAllTask.leanHoldAll st ports values: in the state st, every listed port holds its value as a known bit vector (no x, no z) of exactly the port's width.
HoldAll
/-- `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
svgolf/SvGolf/Task.lean:197Open in code
⊨₂Spec2.leand ⊨₂ P: in every completed run of the design d, under every schedule and every stimulus, the stimulus and the trace satisfy P.
Models2
/-- `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
LeanModels/Sv/Spec2.lean:141Open in code
spec2Spec2.leanTurns a property of a stimulus (one input entry per cycle) and a trace (one snapshot of every signal per cycle) into the form ⊨₂ takes.
spec2
/-- 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
LeanModels/Sv/Spec2.lean:151Open in code
Runs2Spec2.leanRuns2 d σ stim tr: simulating d under the schedule σ on the stimulus stim completes, with the trace tr.
Runs2
/-- 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
LeanModels/Sv/Spec2.lean:121Open in code
ScheduleOracleSemantics.leanA legal schedule: the order in which the simulator runs the processes that are ready at the same moment. The statement quantifies over all of them.
ScheduleOracle
/-- 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
LeanModels/Sv/Semantics.lean:151Open in code
Process2.isEdgePhaseSem2.leanTrue for the processes that run at the clock edge: always_ff, always @(posedge …) and asynchronous-reset blocks.
isEdgePhase
/-- 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
LeanModels/Sv/Sem2.lean:650Open in code

What does not count

Designs that try to score without meeting the statement (negatives), or that exploit a place where the proof's reading and the scored netlist could differ (attacks). The checker stops at the first stage that refuses; each design is listed with that stage.

Refused before any proof is tried

The checker's static stages read the text itself and refuse it: an operator the task bans, a construct that two tools read differently, an unknown (x or z) value that could arise, or a program the language forbids.

  • narrow_out.sv (negative, stopped at extract): The product p is 2N - 1 bits wide instead of 2N.

    the design, with its comments left out
    module mul_n #(parameter int N = 16) (
      input  logic [N-1:0]   a, b,
      output logic [2*N-2:0] p
    );
      logic [2*N-1:0] full;
      assign p = full[2*N-2:0];
      logic [N-1:0][N-1:0] pp;
      logic [N-1:0][N:0]   s;
      logic [N-1:0][N:0]   c;
      logic [N-1:0][N-1:0] t;
    
      genvar i, j;
      generate
        for (i = 0; i < N; i++) begin : g_pp
          for (j = 0; j < N; j++) begin : g_pp_j
            assign pp[i][j] = a[j] & b[i];
          end
        end
      endgenerate
    
      assign s[0] = {1'b0, pp[0]};
      assign c[0] = '0;
      assign t[0] = '0;
    
      generate
        for (i = 1; i < N; i++) begin : g_row
          assign c[i][0] = 1'b0;
          for (j = 0; j < N; j++) begin : g_bit
            assign t[i][j]   = s[i-1][j+1] ^ pp[i][j];
            assign s[i][j]   = t[i][j] ^ c[i][j];
            assign c[i][j+1] = (s[i-1][j+1] & pp[i][j]) | (c[i][j] & t[i][j]);
          end
          assign s[i][N] = c[i][N];
        end
      endgenerate
    
      generate
        for (i = 0; i < N; i++) begin : g_out
          assign full[i] = s[i][0];
        end
      endgenerate
      assign full[2*N-1:N] = s[N-1][N:1];
    endmodule
    
  • mul_star.sv (negative, stopped at banlist): The right function through the banned run-time *.

    the design, with its comments left out
    module mul_n #(parameter int N = 16) (
      input  logic [N-1:0]   a, b,
      output logic [2*N-1:0] p
    );
      assign p = a * b;
    endmodule
    
Rejected at the proof

These pass every static check. The proof that comes with each one does not compile, or does not prove the statement: a design that is wrong somewhere in the task's range (or has other ports) has no proof at all, and a proof that leaves a goal unproved is never accepted.

  • drop_carry.sv (negative, stopped at solution): Every adder row drops its carry out.

    the design, with its comments left out
    module mul_n #(parameter int N = 16) (
      input  logic [N-1:0]   a, b,
      output logic [2*N-1:0] p
    );
      logic [N-1:0][N-1:0] pp;
      logic [N-1:0][N:0]   s;
      logic [N-1:0][N:0]   c;
      logic [N-1:0][N-1:0] t;
    
      genvar i, j;
      generate
        for (i = 0; i < N; i++) begin : g_pp
          for (j = 0; j < N; j++) begin : g_pp_j
            assign pp[i][j] = a[j] & b[i];
          end
        end
      endgenerate
    
      assign s[0] = {1'b0, pp[0]};
      assign c[0] = '0;
      assign t[0] = '0;
    
      generate
        for (i = 1; i < N; i++) begin : g_row
          assign c[i][0] = 1'b0;
          for (j = 0; j < N; j++) begin : g_bit
            assign t[i][j]   = s[i-1][j+1] ^ pp[i][j];
            assign s[i][j]   = t[i][j] ^ c[i][j];
            assign c[i][j+1] = (s[i-1][j+1] & pp[i][j]) | (c[i][j] & t[i][j]);
          end
          assign s[i][N] = 1'b0;
        end
      endgenerate
    
      generate
        for (i = 0; i < N; i++) begin : g_out
          assign p[i] = s[i][0];
        end
      endgenerate
      assign p[2*N-1:N] = s[N-1][N:1];
    endmodule
    
  • only_n16.sv (negative, stopped at solution): Correct only at the scoring width N = 16, zero elsewhere.

    the design, with its comments left out
    module mul_n #(parameter int N = 16) (
      input  logic [N-1:0]   a, b,
      output logic [2*N-1:0] p
    );
      logic [N-1:0][N-1:0] pp;
      logic [N-1:0][N:0]   s;
      logic [N-1:0][N:0]   c;
      logic [N-1:0][N-1:0] t;
    
      genvar i, j;
      generate
        if (N == 16) begin : g_scored
            for (i = 0; i < N; i++) begin : g_pp
              for (j = 0; j < N; j++) begin : g_pp_j
                assign pp[i][j] = a[j] & b[i];
              end
            end
    
          assign s[0] = {1'b0, pp[0]};
          assign c[0] = '0;
          assign t[0] = '0;
    
            for (i = 1; i < N; i++) begin : g_row
              assign c[i][0] = 1'b0;
              for (j = 0; j < N; j++) begin : g_bit
                assign t[i][j]   = s[i-1][j+1] ^ pp[i][j];
                assign s[i][j]   = t[i][j] ^ c[i][j];
                assign c[i][j+1] = (s[i-1][j+1] & pp[i][j]) | (c[i][j] & t[i][j]);
              end
              assign s[i][N] = c[i][N];
            end
    
            for (i = 0; i < N; i++) begin : g_out
              assign p[i] = s[i][0];
            end
          assign p[2*N-1:N] = s[N-1][N:1];
    
        end else begin : g_other
          assign p = '0;
        end
      endgenerate
    endmodule
    
  • top_shift.sv (negative, stopped at solution): The top half of the product is read from the wrong bits.

    the design, with its comments left out
    module mul_n #(parameter int N = 16) (
      input  logic [N-1:0]   a, b,
      output logic [2*N-1:0] p
    );
      logic [N-1:0][N-1:0] pp;
      logic [N-1:0][N:0]   s;
      logic [N-1:0][N:0]   c;
      logic [N-1:0][N-1:0] t;
    
      genvar i, j;
      generate
        for (i = 0; i < N; i++) begin : g_pp
          for (j = 0; j < N; j++) begin : g_pp_j
            assign pp[i][j] = a[j] & b[i];
          end
        end
      endgenerate
    
      assign s[0] = {1'b0, pp[0]};
      assign c[0] = '0;
      assign t[0] = '0;
    
      generate
        for (i = 1; i < N; i++) begin : g_row
          assign c[i][0] = 1'b0;
          for (j = 0; j < N; j++) begin : g_bit
            assign t[i][j]   = s[i-1][j+1] ^ pp[i][j];
            assign s[i][j]   = t[i][j] ^ c[i][j];
            assign c[i][j+1] = (s[i-1][j+1] & pp[i][j]) | (c[i][j] & t[i][j]);
          end
          assign s[i][N] = c[i][N];
        end
      endgenerate
    
      generate
        for (i = 0; i < N; i++) begin : g_out
          assign p[i] = s[i][0];
        end
      endgenerate
      assign p[2*N-1:N] = s[N-1][N-1:0];
    endmodule
    
Spec.lean, verbatim (89 lines)
import SvGolf.Task

/-!
# Task `mul`: N-bit unsigned multiplier (TRUSTED)

The main svGolf challenge. Module (port order as written; `N` is the
module's only parameter):

```systemverilog
module mul_n #(parameter int N = 16) (
  input  logic [N-1:0]   a, b,
  output logic [2*N-1:0] p
);
```

Statement: for every `N` with `1 ≤ N < 2^30` (so that the output width `2N`, an
`int` expression of the module, stays below `2^31`, the values an `int` holds),
the design has exactly the ports `a`, `b` (inputs, `N` bits) and `p`
(output, `2N` bits), no process is clocked, there is no combinational loop,
every run terminates, and in every cycle whose stimulus drives `a` and `b` with
known values the snapshot's `p` is the FULL product `a * b`: the
natural-number product of the two unsigned inputs, as a `2N`-bit vector.
Nothing is truncated: the product of two `N`-bit numbers is below `2^(2N)`
(`fn_toNat` below), so `p` carries it exactly.

Run-time `+ - * / % **` are banned (`task.json`), so the circuit has to be
built from bitwise logic; constant arithmetic (parameter expressions, genvar
indices such as `s[i-1][j+1]`) is fine.
-/

namespace SvGolf.Tasks.Mul

open SvGolf

/-- The task: inputs `a b : BitVec N`, output `p = a * b` on `2N` bits, for
`1 ≤ N < 2^30`. -/
def task : CombTask where
  inRange N := 1 ≤ N ∧ N < 2 ^ 30
  inputs N := [("a", N), ("b", N)]
  outputs N := [("p", 2 * N)]
  fn N := fun (a, b, ()) => (BitVec.ofNat (2 * N) (a.toNat * b.toNat), ())

/-- **The challenge statement.** A submission proves
`SvGolf.Tasks.Mul.Statement Submission.design.sem`. -/
abbrev Statement : (Nat → LeanModels.Sv.Design2) → Prop := task.Statement

/-! Sanity checks on the statement's data. -/

/-- The scoring point `N = 16` (and the reported points 8 and 32) are in range. -/
example : task.inRange 16 ∧ task.inRange 8 ∧ task.inRange 32 := by simp [task]

/-- The range ends below `2^30`: the output width `2N` (the port's `[2*N-1:0]`, evaluated as an
`int`) stays below `2^31`, the values an `int` holds. -/
example : task.inRange (2 ^ 30 - 1) ∧ ¬ task.inRange (2 ^ 30) := by simp [task]

/-- At the top of the range the output width is still an `int`. -/
example : 2 * (2 ^ 30 - 1) + 1 < 2 ^ 31 := by decide

/-- The input names are distinct (the non-vacuity theorem
`CombTask.SatisfiedAt.exists_run` needs it), at every `N`. -/
example (N : Nat) : ((task.inputs N).map Prod.fst).Nodup := by
  simp [task]

/-- **No truncation**: the output is exactly the natural-number product. -/
theorem fn_toNat (N : Nat) (a b : BitVec N) :
    (task.fn N (a, b, ())).1.toNat = a.toNat * b.toNat := by
  show (BitVec.ofNat (2 * N) (a.toNat * b.toNat)).toNat = a.toNat * b.toNat
  rw [BitVec.toNat_ofNat, Nat.mod_eq_of_lt]
  rw [Nat.two_mul, Nat.pow_add]
  exact Nat.mul_lt_mul_of_lt_of_lt a.isLt b.isLt

/-- The same function, read as `BitVec` multiplication at double width. -/
theorem fn_eq_mul (N : Nat) (a b : BitVec N) :
    (task.fn N (a, b, ())).1 = a.setWidth (2 * N) * b.setWidth (2 * N) := by
  apply BitVec.eq_of_toNat_eq
  have hlt : ∀ x : BitVec N, x.toNat < 2 ^ (2 * N) := fun x =>
    Nat.lt_of_lt_of_le x.isLt (Nat.pow_le_pow_right (by decide) (by omega))
  rw [fn_toNat, BitVec.toNat_mul, BitVec.toNat_setWidth, BitVec.toNat_setWidth,
    Nat.mod_eq_of_lt (hlt a), Nat.mod_eq_of_lt (hlt b), Nat.mod_eq_of_lt]
  rw [Nat.two_mul, Nat.pow_add]
  exact Nat.mul_lt_mul_of_lt_of_lt a.isLt b.isLt

/-- Concrete values: `15 * 15 = 225` at `N = 4`, `255 * 255 = 65025` at
`N = 8`, `3 * 3 = 9` at `N = 2` (the top bit is used). -/
example : (task.fn 4 (15#4, 15#4, ())).1 = 225#8 := by decide
example : (task.fn 8 (255#8, 255#8, ())).1 = 65025#16 := by decide
example : (task.fn 2 (3#2, 3#2, ())).1 = 9#4 := by decide

end SvGolf.Tasks.Mul

Its trusted text

The modules specific to this task. Like every task, it also rests on the shared trusted base (the ls-sv semantics of SystemVerilog and svGolf's statement helpers), and a proof may import any module of the ls-sv library. Nothing else is trusted: proof helpers are kernel-checked like any submission.

modulerolelinessha256
tasks.mul.Specsvgolf/tasks/mul/Spec.leantask statement898791b67eebf4

How to participate #

  1. Write a circuit with the task's exact interface.
  2. Prove in Lean 4 that it meets the trusted statement; Start a new design, below, gives the route, and the proving guide shows how.
  3. Check and record it (how to submit): the checker accepts the pair or says which stage refused it, as it does for each design under what does not count.
  4. Beat par: an accepted design whose score (area × time) is below that of the best proved reference. The leaderboard ranks by it.

How to submit

You hand in two files: solution.sv, a module with the exact header under Interface, and Solution.lean, a Lean 4 proof that it meets the trusted statement.

Step 0, once per machine. Install:

  • elan, which installs the Lean toolchain that lean-toolchain pins (leanprover/lean4:v4.33.0-rc1) on first use;
  • Yosys 0.53 with its yosys-abc (every pinned tool and file: Reproduce a score);
  • Verilator 5, a C++ compiler and make, for the post-synthesis check (it compiles Verilator's model of the netlist);
  • Python 3.12 with python3.12 -m pip install -r requirements.txt (pyslang==11.0.0, the extractor's SystemVerilog frontend).
step 0
git clone https://github.com/thomasnormal/svgolf
cd svgolf
python3.12 -m pip install -r requirements.txt
# the trusted Lean and the checker's evaluator
lake build
# the pinned sky130 liberty, checked by its hash
python3.12 score/ppa.py --fetch-liberty

lake build takes about 6 minutes and 1 GB of disk the first time, on a laptop that already had the Lean toolchain (README.md's Step 0 says what was measured where; elan's first download of the toolchain comes on top): Lake clones proglib (formerly lean-surfaces) and its packages from github.com, so it needs network access there, and builds the LeanModels.Sv modules svGolf uses; Mathlib is cloned, never built.

Start from the reference pair under Files, mul_tree_h, a complete and accepted one: copy it, from the same directory:

shell
cp tasks/mul/reference/mul_tree_h.sv solution.sv
cp tasks/mul/reference/mul_tree_h.lean Solution.lean

Then, for each design, from the same directory: find it without Lean and check it once without a proof (the golfing loop and step 1), write its proof (Start a new design, below, gives the route), then check the pair and record it (steps 2 and 3):

shell
# the golfing loop, no Lean: area, delay and the critical
#    path, about two seconds
python3.12 score/ppa.py solution.sv --task tasks/mul \
    --no-mapped --path
# 1. no proof needed; never accepts; exits 0 when every
#    stage passed, ending with area x time and par
python3.12 check.py tasks/mul solution.sv --unproved \
    --tactics --stage-dir stage
# 2. the real check: prints "accepted": true; its
#    post-synthesis check is LIGHT by default (a subset of
#    the vectors, for local feedback), so stderr also says
#    "accepted, but the check was INCOMPLETE ... rerun with
#    --postsynth full": expected, since step 3 runs the
#    full one itself
python3.12 check.py tasks/mul solution.sv Solution.lean
# 3. record it: the whole check again, full and sandboxed
python3.12 leaderboard/leaderboard.py add --task mul \
    --player YOUR_HANDLE --assisted-by "TOOL OR MODEL" \
    --sv solution.sv --lean Solution.lean \
    --notes "YOUR DESIGN IN ONE LINE"

Done: step 3 prints added mul/HASH: YOUR_HANDLE, … once the pair has passed the whole check, full and sandboxed, and its record is written (what it writes, and where: below); otherwise it prints REFUSED with the reasons and records nothing. Step 2's "accepted": true, with its INCOMPLETE line, is feedback for you, not a record.

The golfing loop and step 1, the first two commands above. The second command runs every stage that needs no proof, the task's oracle included (a wrong circuit is named there, not deep in a proof), says whether the proof tactics read the design (tactics.in_fragment) and leaves it staged in stage/ for probe files (PROVING.md, section 0).

score/ppa.py is the inner loop of golfing; its ranked numbers are area_um2_sky130 and delay_ps_sky130 in what it prints. It runs only the source gate and the pinned flow: it does not check the banlist, the 4-state rules, whether ls-sv can read the design, the post-synthesis simulation or the task's function. Step 1 does, still without a proof.

The Lean file. The checker extracts your .sv itself and writes the trusted module Submission.Design; Goal unfolds to SvGolf.Tasks.Mul.Statement Submission.design.sem, the task's statement of your design:

Submission.Design (generated: you never write it)
import LeanModels.Sv.Ingest2
import tasks.mul.Spec

namespace Submission
load_design_sv2 design from "<the envelope of your .sv>"
abbrev Goal : Prop := SvGolf.Tasks.Mul.Statement design.sem
end Submission

Your Solution.lean imports it and proves Goal, as the skeleton in Start a new design (The proof in detail) shows. File names and paths are free (here solution.sv and Solution.lean); the module name Submission, the import Submission.Design header and the theorem solution : Goal (in namespace Submission, theorem Submission.solution : Submission.Goal) are fixed.

Step 3 writes your record's three files (.json, .sv and .lean, named by the record's hash) to leaderboard/data/mul/, the directory a pull request adds. Then open a pull request against https://github.com/thomasnormal/svgolf that adds them; CI re-verifies it. There is no submission server: nothing is sent anywhere by these commands. For a private trial, add --data-dir DIR: the record then goes to DIR and the board stays as it is. Repeat --assisted-by for each tool or AI system that helped, or leave it out.

YOUR_HANDLE is your public name: the board and the record's page show it. It is 1 to 32 characters, letters, digits, ., _ or -, the first a letter or a digit ([A-Za-z0-9][A-Za-z0-9._-]{0,31}). --notes is optional: one line on the design (at most 2000 characters), shown on the record's page. In a private trial (--data-dir DIR), add --from-check RESULT.json records the JSON that a check.py … --postsynth full --ppa require run printed instead of checking again; the board's own leaderboard/data/ refuses a stored result.

the sandbox, and machines without one

leaderboard.py add runs the check with --sandbox require: the proof's Lean, the extractor, Yosys and Verilator must run inside the operating system's sandbox (Seatbelt on macOS, bubblewrap on Linux). Where that sandbox is missing or fails its self-test, the check stops with an infrastructure error and nothing is recorded. On such a machine, --sandbox auto runs the check without it (and says so on stderr), and --sandbox off skips it: your proof file then runs with your own privileges, and the record is for your own use, since an official record is made with the sandbox or not at all. add also weighs the ranked sky130 numbers, which need the pinned liberty (Step 0). On a shared machine whose Lean calls go through a wrapper, set SVGOLF_LEAN_WRAP; check.py and leaderboard.py both read it, and SVGOLF_LEAN_WRAP_ONCE=1 takes the wrapper once for a whole run.

What moves the score

What players have seen move the ranked sky130 numbers: observations, not promises (the pinned flow is the judge, and score/ppa.py measures a variant in seconds).

  • The Boolean form of a cell moves both area and delay: the association of a three-input XOR, a ?: majority, which input of a 4:2 compressor enters late.
  • The pinned flow's ABC script restructures what you write: a structural trick that ABC can factor back into the plain form does not survive.
  • Commuting the operands of one operator moves the A0' area by up to 11% and the delay by up to 32% (the maintainers' measurement, score/PPA.md).
  • The golfing loop's --path names the critical path with each net's arrival time: read it before you guess.

Leaderboard

by score · ASAP7
  1. 1
    2026-11-05
    42 5307961.4 µm² × 5.34 ns
  2. 2
    2026-11-05
    50 4987978.9 µm² × 6.33 ns
  3. ⚑
    par proved reference
    51 3118563.2 µm² × 5.99 ns
every column: area, time, proof lines, assisted by, files
#playerdatescore (µm²·ns)area (µm²)time (ps)area (GE)depthproof linesassisted byrecordfiles
1late-carry2026-11-0542 5307961.453421930.30060360Claude Sonnet 5.5 (Anthropic)5b30f35c283b.sv .lean
2plus-minus-two2026-11-0550 4987978.963291875.89264768Claude Opus 5.5 (Anthropic)adce0bcea630.sv .lean
  1. 1
    2026-11-05
    131.5120.854 µm² × 1.09 ns
  2. ⚑
    par proved reference
    148.4127.954 µm² × 1.16 ns
  3. 2
    2026-11-05
    156.5119.746 µm² × 1.31 ns
every column: area, time, proof lines, assisted by, files
#playerdatescore (µm²·ns)area (µm²)time (ps)area (GE)depthproof linesassisted byrecordfiles
1late-carry2026-11-05131.5120.85410881930.30060360Claude Sonnet 5.5 (Anthropic)5b30f35c283b.sv .lean
2plus-minus-two2026-11-05156.5119.74613071875.89264768Claude Opus 5.5 (Anthropic)adce0bcea630.sv .lean
score: area × time, µm²·nspar

At a glance

top module
mul_n
parameters
N in range 1 <= N < 2^30
scoring point
N = 16
depth budget
none
banned run-time operators
+ - * / % **, and the run-time ordering comparisons < <= > >= (Yosys builds each as its own adder, an $alu; equality stays allowed)

Yosys's alumacc merges + chains into the same $macc cell as *, so every arithmetic operator is banned at run time; constant arithmetic (parameters, genvar indices) stays allowed.

allowed axioms
propext, Classical.choice, Quot.sound
proof time limit
300 s of CPU time (a wall-clock backstop of 1500 s stops a proof that waits)

Start a new design

The route: the sv_comb tactics, then the arithmetic of your rows (PROVING.md, sections 1 to 3; complete proofs: reference/mul_n.lean, the array, 70 code lines; reference/mul_wallace.lean, a Wallace tree as a tape of rows, 264; reference/mul_tree_h.lean, par):

  1. sv_structure (lexRank := key N): one driver per bit and no combinational loop (CombDesign ∧ Acyclic), ordered by your key, a lexicographic triple per site (mulKey in mul_n.lean: row, column, phase).
  2. sv_interface: the ports, one line.
  3. sv_equations: each assign as an equation over Bool and every driven bit known; 2-D signals are read as st.cell "T" W r c, and each field of your arithmetic lemma is one sv_bridge eq_… line.
  4. The arithmetic, in plain Lean: the rows give p = a * b. An array is CombLib.MulNetlistView and ideal_mul_view; a tree proves the value of its leaf rows and that its reduction keeps the sum. A radix-4 Booth multiplier (accepted 2026-11-05) proved each Booth row's value, the recoding identity by induction on the rows, and reused the Wallace tape of mul_wallace.lean for the reduction.

reference/mul_tree_h.lean, named above, is 307 code lines long.

The proof in detailLay the design out so the tactics take it · What sv_equations prints · The skeleton · Library lemmas that exist today

Lay the design out so the tactics take it (what they refuse, with the messages: PROVING.md, section 4):

  • continuous assigns of gates (& | ^ ~, ?: and the other operators section 4 lists) in at most two nested generate for loops, every index and loop bound affine in N and the genvars;
  • rows as the rows of 2-D arrays, T[r][c]; sv_cover finds the assign that writes a read by solving one unknown per index equation, so a read whose writer needs r = i / W and c = i % W is not covered;
  • counts derived from N as integer localparams (mul_tree_h.sv's localparam integer H = (N + 1) / 2;; a 2-state int with / or % is refused);
  • every declared bit driven at N = 16; a constant row gets a literal;
  • a read of a zero-padded copy of an input keeps every index affine where a row starts at a moving column (the tree references' ae, read as ae[c + 2*N - 1 - j] & b[j]).

What sv_equations prints, the shapes the arithmetic lemma is written against (sv_equations? in a probe file lists them for your design):

  • a part select bb[N:1] = b as bb (Int.toNat 1 + k) = b k; a cell of row 0, T[0][c], as the flat cell T c (sv_bridge matches it);
  • a family that writes a constant row in a column loop leaves a True ∧ … that omega refuses after sv_rowmajor: close the structure goals with first | omega | (simp only [true_and, and_true] at *; omega);
  • write T 0 c = (x || y) with the parentheses: = binds tighter than ||, and Lean accepts the unparenthesized hypothesis silently (as a decide), which sv_bridge then reports as different shapes.

The skeleton (reference/mul_n.lean is it, filled in for the array):

import Submission.Design
import SvGolf.Comb
import LeanModels.Sv.CombEqTactic
import LeanModels.Sv.CombLib

namespace Submission
open LeanModels.Sv LeanModels.Sv.CombView LeanModels.Sv.CombLib LeanModels.Sv.CombEq SvGolf

set_option maxHeartbeats 400000 in
theorem my_struct (N : Nat) (hN : 1 ≤ N) :
    CombDesign (design.sem N) ∧ Acyclic (design.sem N) ∧ SharedObl design.pdesign [N] := by
  sv_structure (lexRank := myKey N)           -- your key, a `def` above (mul_n.lean's `mulKey`)
  all_goals (intros; sv_rowmajor; omega)

theorem my_interface (N : Nat) (hN : 1 ≤ N) (hc : CombDesign (design.sem N)) :
    SvGolf.Tasks.Mul.task.Interface N (design.sem N) := by
  sv_interface

-- your arithmetic: the rows' equations in, the product out (a structure of fields, as `MulNetlistView`)

set_option maxHeartbeats 400000 in
theorem solution : Goal := by
  intro N hN
  have hN' : 1 ≤ N := hN.1
  obtain ⟨hc, ha, hso⟩ := my_struct N hN'
  refine CombTask.satisfiedAt_of_comb (my_interface N hN' hc) hc ha ?_
  intro st heqs hs v hv
  obtain ⟨x, y, ⟨⟩⟩ := v
  simp only [SvGolf.Tasks.Mul.task, HoldAll_cons, HoldAll_nil, and_true] at hv ⊢
  obtain ⟨hA, hB⟩ := hv
  sv_equations
  · -- the main goal: your arithmetic lemma, one `sv_bridge eq_…` per field (mul_n.lean)
  case inputsKnown_1 => exact inKnown_ofBitVec hA (by omega)
  case inputsKnown_2 => exact inKnown_ofBitVec hB (by omega)
  all_goals first | (intros; omega) | sv_cover

end Submission

Library lemmas that exist today (the tables: PROVING.md, section 6):

  • LeanModels.Sv.CombLib: MulNetlistView, ideal_mul and ideal_mul_view (the array's equations give the product), the bit-function value bfVal and its lemmas, fullAdder_arith, ripple_row (a final ripple adder), sum_split, bitvec_eq_ofNat_of_val, rowMajor_inj, lexLt;
  • LeanModels.Sv.Lib.XFVal: val_csa (a carry-save row: sum plus twice the carry is the three inputs) and the value of windows, concatenations, extensions and constants;
  • SvState.cell, sv_bridge and sv_rowmajor; rmN and Key (LeanModels/Sv/CombViewArith.lean) and LeanModels.Sv.Lib.RowMajor; the port bridges SvState.lookup_eq_ofBitVec_bv, SvState.boolAt_ofBitVec, inKnown_ofBitVec;
  • the reductions are proved inside the references (a task ships no library: copy them): mul_wallace.lean's csa_row, csum and tape_mul; mul_tree_h.lean's node_sum, lane_val (a row read from the padded copy) and heap_mul.

Planned, not in the library yet: the Booth multiplier's generic carry-save tape theorem (any leaf rows on a Wallace tape sum to the leaves modulo 2^W) and its bfVal lemmas for rows built from windows and single bits.

Compare the designs

ASAP7 · any two numbers · lower is better

N = 16, the scored point: the ranked numbers. The same designs are also measured at N = 4, N = 8 and N = 32: measured, not ranked, the same proved designs through the same pinned flow at other parameters (the tables below, and how the designs scale).

  • submission
  • proved reference
  • baseline (not proved)
  • baseline with a banned operator
  • the Pareto front of the proved designs
Lower is better on both axes. The step line joins the proved designs that no other proved design beats on both; baselines are shown for scale. Choose any two numbers to find the best design for your own trade-off. 10 of 10 designs have both numbers.
every design with all its numbers, by score (at N = 16, ranked)
designScore (µm²·ns)Area (µm²)Delay (ps)Energy (fJ/op)Leakage (nW)Area (GE)Depth (gates)GatesProof (lines)
late-carry submission42 5307961.45342––1930.300601431360
dadda_rc_16 not proved44 9468230.4546114 61841953.585611408–
dadda_ks_16 not proved46 0098937.3514816 72742076.510481483–
plus-minus-two submission50 4987978.96329––1875.892641345768
mul_tree_h par51 3118563.2599216 84942025.905621450307
mul_tree reference52 7298603.3612916 90342034.904631456322
mul_wallace reference53 6358966.1598216 06742113.894621502264
yosys_mul banned operator55 2598826.0626116 32442081.227591486–
mul_n reference90 4248290.510 90737 60041962.57688140870
mul_n_byhand reference90 4248290.510 90737 60041962.5768814081385
at N = 4 (measured, not ranked): every design with all its numbers, by score
designScore (µm²·ns)Area (µm²)Delay (ps)Area (GE)Depth (gates)GatesProof (lines)
mul_tree reference675.9380.4177788.9941464322
mul_tree_h reference675.9380.4177788.9941464307
yosys_mul banned operator683.8387.9176389.6591264–
mul_wallace reference753.9410.4183794.9921368264
mul_n reference986397.9247891.326166470
mul_n_byhand reference986397.9247891.32616641385
at N = 8 (measured, not ranked): every design with all its numbers, by score
designScore (µm²·ns)Area (µm²)Delay (ps)Area (GE)Depth (gates)GatesProof (lines)
dadda_rc_8 not proved61811891.83267445.97829320–
mul_tree reference62841911.83287451.31030324322
mul_tree_h reference62841911.83287451.31030324307
dadda_ks_8 not proved69952105.83322484.29024345–
mul_wallace reference70012053.23410482.97430343264
yosys_mul banned operator74932001.93743468.63730334–
mul_n reference99771904.35239447.9764032070
mul_n_byhand reference99771904.35239447.976403201385
at N = 32 (measured, not ranked): every design with all its numbers, by score
designScore (µm²·ns)Area (µm²)Delay (ps)Area (GE)Depth (gates)GatesProof (lines)
dadda_rc_32 not proved337 61934 237.898618153.0201255888–
mul_tree_h reference379 12035 661.710 6318452.6261276058307
mul_tree reference380 06435 666.710 6568453.6261266058322
mul_wallace reference382 05437 081.810 3038767.5951246245264
yosys_mul banned operator400 59336 533.810 9658639.2541106185–
mul_n reference767 76534 515.622 2448191.648184588870
mul_n_byhand reference767 76534 515.622 2448191.64818458881385
  • submission
  • proved reference
  • baseline (not proved)
  • baseline with a banned operator
  • the Pareto front of the proved designs
Lower is better on both axes. The step line joins the proved designs that no other proved design beats on both; baselines are shown for scale. Choose any two numbers to find the best design for your own trade-off. 10 of 10 designs have both numbers.
every design with all its numbers, by score
designScore (µm²·ns)Area (µm²)Delay (ps)Area (GE)Depth (gates)GatesProof (lines)
dadda_ks_16 not proved120.1133.1749022076.510481483–
late-carry submission131.5120.85410881930.300601431360
dadda_rc_16 not proved135.7123.39111001953.585611408–
mul_tree_h par148.4127.95411602025.905621450307
yosys_mul banned operator150.1131.78911392081.227591486–
mul_tree reference153.1128.55211912034.904631456322
plus-minus-two submission156.5119.74613071875.892641345768
mul_wallace reference158.8133.91711862113.894621502264
mul_n reference245.5123.93019811962.57688140870
mul_n_byhand reference245.5123.93019811962.5768814081385

How the designs scale #

On sky130 in either view: the designs are measured at the other points on sky130 alone (score/measured/), and the ASAP7 view weighs each design at the scoring point only.

mul: score over par against N, one line per design7 designs, Score ÷ par's (y) against N (x), one line each, named on the right; lower is better. The table under the chart lists the same numbers.11.251.51.752481632N, log scaleScore ÷ par's · lower is betterpar: mul_tree_hdadda_ks · baseline, not proved · measured, not ranked · 2 sizes of one generator: dadda_ks_16, dadda_ks_8dadda_ks · N = 8 · measured, not ranked · Score: 6995 µm²·ns (×1.11 par)dadda_ks · N = 16 · the scored point · Score: 46 009 µm²·ns (×0.90 par)dadda_ksdadda_rc · baseline, not proved · measured, not ranked · 3 sizes of one generator: dadda_rc_16, dadda_rc_32, dadda_rc_8dadda_rc · N = 8 · measured, not ranked · Score: 6181 µm²·ns (×0.98 par)dadda_rc · N = 16 · the scored point · Score: 44 946 µm²·ns (×0.88 par)dadda_rc · N = 32 · measured, not ranked · Score: 337 619 µm²·ns (×0.89 par)dadda_rcyosys_mul · baseline, not proved · banned operator · measured, not rankedyosys_mul · N = 4 · measured, not ranked · Score: 683.8 µm²·ns (×1.01 par)yosys_mul · N = 8 · measured, not ranked · Score: 7493 µm²·ns (×1.19 par)yosys_mul · N = 16 · the scored point · Score: 55 259 µm²·ns (×1.08 par)yosys_mul · N = 32 · measured, not ranked · Score: 400 593 µm²·ns (×1.06 par)yosys_mulmul_n +1 · proved reference · measured, not ranked · the same numbers at every point: mul_n, mul_n_byhandmul_n +1 · N = 4 · measured, not ranked · Score: 986 µm²·ns (×1.46 par)mul_n +1 · N = 8 · measured, not ranked · Score: 9977 µm²·ns (×1.59 par)mul_n +1 · N = 16 · the scored point · Score: 90 424 µm²·ns (×1.76 par)mul_n +1 · N = 32 · measured, not ranked · Score: 767 765 µm²·ns (×2.03 par)mul_n +1mul_tree · proved reference · measured, not rankedmul_tree · N = 4 · measured, not ranked · Score: 675.9 µm²·ns (×1.00 par)mul_tree · N = 8 · measured, not ranked · Score: 6284 µm²·ns (×1.00 par)mul_tree · N = 16 · the scored point · Score: 52 729 µm²·ns (×1.03 par)mul_tree · N = 32 · measured, not ranked · Score: 380 064 µm²·ns (×1.00 par)mul_treemul_tree_h · proved reference · measured, not rankedmul_tree_h · N = 4 · measured, not ranked · Score: 675.9 µm²·ns (×1.00 par)mul_tree_h · N = 8 · measured, not ranked · Score: 6284 µm²·ns (×1.00 par)mul_tree_h · N = 16 · the scored point · Score: 51 311 µm²·ns (×1.00 par)mul_tree_h · N = 32 · measured, not ranked · Score: 379 120 µm²·ns (×1.00 par)mul_tree_hmul_wallace · proved reference · measured, not rankedmul_wallace · N = 4 · measured, not ranked · Score: 753.9 µm²·ns (×1.12 par)mul_wallace · N = 8 · measured, not ranked · Score: 7001 µm²·ns (×1.11 par)mul_wallace · N = 16 · the scored point · Score: 53 635 µm²·ns (×1.05 par)mul_wallace · N = 32 · measured, not ranked · Score: 382 054 µm²·ns (×1.01 par)mul_wallace
mul: score over par against N, one line per design7 designs, Score ÷ par's (y) against N (x), one line each, named on the right; lower is better. The table under the chart lists the same numbers.11.52481632N, log scaleScore ÷ par's · lower is betterpar: mul_tree_hdadda_ks · baseline, not proved · measured, not ranked · 2 sizes of one generator: dadda_ks_16, dadda_ks_8dadda_ks · N = 8 · measured, not ranked · Score: 6995 µm²·ns (×1.11 par)dadda_ks · N = 16 · the scored point · Score: 46 009 µm²·ns (×0.90 par)dadda_ksdadda_rc · baseline, not proved · measured, not ranked · 3 sizes of one generator: dadda_rc_16, dadda_rc_32, dadda_rc_8dadda_rc · N = 8 · measured, not ranked · Score: 6181 µm²·ns (×0.98 par)dadda_rc · N = 16 · the scored point · Score: 44 946 µm²·ns (×0.88 par)dadda_rc · N = 32 · measured, not ranked · Score: 337 619 µm²·ns (×0.89 par)dadda_rcyosys_mul · baseline, not proved · banned operator · measured, not rankedyosys_mul · N = 4 · measured, not ranked · Score: 683.8 µm²·ns (×1.01 par)yosys_mul · N = 8 · measured, not ranked · Score: 7493 µm²·ns (×1.19 par)yosys_mul · N = 16 · the scored point · Score: 55 259 µm²·ns (×1.08 par)yosys_mul · N = 32 · measured, not ranked · Score: 400 593 µm²·ns (×1.06 par)yosys_mulmul_n +1 · proved reference · measured, not ranked · the same numbers at every point: mul_n, mul_n_byhandmul_n +1 · N = 4 · measured, not ranked · Score: 986 µm²·ns (×1.46 par)mul_n +1 · N = 8 · measured, not ranked · Score: 9977 µm²·ns (×1.59 par)mul_n +1 · N = 16 · the scored point · Score: 90 424 µm²·ns (×1.76 par)mul_n +1 · N = 32 · measured, not ranked · Score: 767 765 µm²·ns (×2.03 par)mul_n +1mul_tree · proved reference · measured, not rankedmul_tree · N = 4 · measured, not ranked · Score: 675.9 µm²·ns (×1.00 par)mul_tree · N = 8 · measured, not ranked · Score: 6284 µm²·ns (×1.00 par)mul_tree · N = 16 · the scored point · Score: 52 729 µm²·ns (×1.03 par)mul_tree · N = 32 · measured, not ranked · Score: 380 064 µm²·ns (×1.00 par)mul_treemul_tree_h · proved reference · measured, not rankedmul_tree_h · N = 4 · measured, not ranked · Score: 675.9 µm²·ns (×1.00 par)mul_tree_h · N = 8 · measured, not ranked · Score: 6284 µm²·ns (×1.00 par)mul_tree_h · N = 16 · the scored point · Score: 51 311 µm²·ns (×1.00 par)mul_tree_h · N = 32 · measured, not ranked · Score: 379 120 µm²·ns (×1.00 par)mul_tree_hmul_wallace · proved reference · measured, not rankedmul_wallace · N = 4 · measured, not ranked · Score: 753.9 µm²·ns (×1.12 par)mul_wallace · N = 8 · measured, not ranked · Score: 7001 µm²·ns (×1.11 par)mul_wallace · N = 16 · the scored point · Score: 53 635 µm²·ns (×1.05 par)mul_wallace · N = 32 · measured, not ranked · Score: 382 054 µm²·ns (×1.01 par)mul_wallace
  • proved reference
  • baseline (not proved)
  • baseline with a banned operator
  • par
Score of each design over par's (mul_tree_h) at the same point, against N: 1 is par, and a line under it beats par at that size, so where two lines cross one design starts to pay off against the other. One line per design, lower is better: the same proved design at each point, through the same pinned flow. Measured, not ranked: the board ranks at N = 16 only. A dashed line is a baseline (not proved). A name with +1, +2, … stands for that many more designs with the same numbers at every point (the table names them). dadda_ks and dadda_rc are families of fixed-size baselines, one file per size, each drawn as one line. Not measured: dadda_ks_32 at N = 32 (the pinned flow does not finish within the task's 120 s time limit).
every design at every point: Score (µm²·ns)
designN = 4N = 8N = 16 (ranked)N = 32
mul_n +1 mul_n, mul_n_byhand986997790 424767 765
mul_tree675.9628452 729380 064
mul_wallace753.9700153 635382 054
mul_tree_h675.9628451 311379 120
dadda_ks dadda_ks_16, dadda_ks_8–699546 009–
dadda_rc dadda_rc_16, dadda_rc_32, dadda_rc_8–618144 946337 619
yosys_mul683.8749355 259400 593

Proved references

Proved and accepted designs that ship with the task, written or adapted by the maintainers. They are the first entries, and par, the bar to beat, is the one with the best score (area × time, µm²·ns, from the sky130ASAP7 area and time; a design without a sky130an ASAP7 weighing has no score and comes after, by its extra GE columns). A reference that was also recorded links its record under its name: the record holds that reference's design and proof, matched by the sha256 of both files.

designscore (µm²·ns)area (µm²)time (ps)GE · depthvs parproof linesfiles
mul_n The N × N array multiplier, partial products summed by rows of full adders, proved with the sv_comb tactics.tactics: omega, simp, sv_cover, sv_equations, sv_interface, sv_rowmajor, sv_structure90 424245.58290.5123.93010 90719811962.576 · 88+76.2%+65.4%70.sv .lean
mul_n_byhand same design as mul_nThe same design as mul_n, proved by hand without the tactics: the by-hand proof.tactics: decide, grind, induction, omega, simprecord: 3129b4b881ab90 424245.58290.5123.93010 90719811962.576 · 88+76.2%+65.4%1385.sv .lean
mul_tree A log-depth carry-save tree of 4:2 compressors over the partial-product rows, with a ripple final adder.tactics: grind, induction, omega, simp, sv_cover, sv_equations, sv_interface, sv_rowmajor, sv_structure52 729153.18603.3128.552612911912034.904 · 63+2.8%+3.2%322.sv .lean
mul_wallace A Wallace tree: carry-save adders reduce the rows three at a time, then a ripple final adder.tactics: grind, induction, omega, simp, sv_cover, sv_equations, sv_interface, sv_rowmajor, sv_structure53 635158.88966.1133.917598211862113.894 · 62+4.5%+7.0%264.sv .lean
mul_tree_h parparA carry-save tree whose leaves each hold two partial-product rows, every node a 4:2 compressor, with a ripple final adder.tactics: grind, induction, omega, simp, sv_cover, sv_equations, sv_interface, sv_rowmajor, sv_structurerecord: a24c9112711651 311148.48563.2127.954599211602025.905 · 62parpar307.sv .lean

Baselines

Not entries. Designs scored by the pinned flow (with the banlist lifted) but never accepted: unproved, simulation-only, or using an operator the task bans. They show how much room there is.

designscore (µm²·ns)area (µm²)time (ps)GE · depthvs parfile
dadda_rc_16 A Dadda tree with a ripple final adder, generated for N = 16 only and checked by simulation.44 946135.78230.4123.391546111001953.585 · 61−12.4%−8.6%.sv
dadda_ks_16 A Dadda tree with a Kogge-Stone final adder, generated for N = 16 only and checked by simulation.46 009120.18937.3133.17451489022076.510 · 48−10.3%−19.1%.sv
yosys_mul banned operatorYosys's own multiplier, assign p = a * b;, which uses the banned *.55 259150.18826.0131.789626111392081.227 · 59+7.7%+1.1%.sv
dadda_ks_32 A Dadda tree with a Kogge-Stone final adder, generated for N = 32 only; not scored, because synthesis ran past the flow's time limit.––––––not scored––.sv
dadda_ks_8 A Dadda tree with a Kogge-Stone final adder, generated for N = 8 only and checked by simulation.––––––scored at N=8 only––.sv
dadda_rc_32 A Dadda tree with a ripple final adder, generated for N = 32 only and checked by simulation.––––––scored at N=32 only––.sv
dadda_rc_8 A Dadda tree with a ripple final adder, generated for N = 8 only and checked by simulation.––––––scored at N=8 only––.sv