Tasks / trusted base

tasks.mul.Spec

task statement

Lines
89
file
svgolf/tasks/mul/Spec.lean
sha256
8791b67eebf44034f5d825561fc6d472bae2347a7ba3a75461ebd4dc59d00880
statements that rely on it
mul
download
tasks.mul.Spec.lean
svgolf/tasks/mul/Spec.leantasks.mul.Spec.lean
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