ai-free / backend /wasm /Wasm /Isa.lean
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
f5deffb verified
Raw History Blame Contribute Delete
10.5 kB
/-!
# Wasm.Isa
A model of the WebAssembly subset the backend emits, following the WebAssembly core
specification:
* typed operand stack (`i32` / `i64`), mutable globals, linear memory, a function table;
* `i64.load`/`i64.store` on **bytes** (little-endian), taking an `i32` address plus a static offset
computed in unbounded arithmetic; an access with any byte outside the memory traps. There is no
aliasing assumption: overlapping accesses are modelled exactly;
* `i64.div_s` traps on division by zero and on `INT64_MIN / -1`; `i64.div_u` traps on division by
zero; shifts and rotates take the count modulo 64 (the spec's definition);
* `if` (without `else`) and `loop`/`br` with the spec's label discipline, including unwinding the
operand stack to the label height on a branch; `call_indirect` through the function table (a
callee runs on a fresh operand stack and returns the single value it leaves).
Two host calls are terminal pseudo-instructions: `exitTrap c` (`i32.const c; call $proc_exit`) and
`exitHalt` (`global.get sp; call $halt`, which dumps the machine state and exits 0).
Semantics is a big-step relation, so loops need no fuel. That the model matches a real
WebAssembly engine is an assumption, tested by differential execution (`wasmw`).
-/
namespace WordDialect
namespace Wasm
abbrev W := BitVec 64
abbrev W32 := BitVec 32
inductive Val where
| i32 (v : W32)
| i64 (v : W)
deriving DecidableEq
/-- Little-endian read of `n` bytes starting at `a` (each byte taken modulo 256). -/
def rdBytes (b : Nat β†’ Nat) (a : Nat) : Nat β†’ Nat
| 0 => 0
| n + 1 => rdBytes b a n + (b (a + n) % 256) * 256 ^ n
structure WMem where
bytes : Nat β†’ Nat
size : Nat
def WMem.read64 (m : WMem) (a : Nat) : Option W :=
if a + 8 ≀ m.size then some (BitVec.ofNat 64 (rdBytes m.bytes a 8)) else none
def WMem.write64 (m : WMem) (a : Nat) (v : W) : Option WMem :=
if a + 8 ≀ m.size then
some { m with bytes := fun p => if a ≀ p ∧ p < a + 8 then (v.toNat / 256 ^ (p - a)) % 256 else m.bytes p }
else none
structure WState where
stack : List Val
globals : Nat β†’ Val
mem : WMem
inductive WI where
| i32const (v : W32)
| i64const (v : W)
| globalGet (i : Nat)
| globalSet (i : Nat)
| i64load (off : Nat)
| i64store (off : Nat)
| i64add | i64sub | i64mul | i64and | i64or | i64xor
| i64shl | i64shru | i64rotl | i64rotr | i64divu | i64divs
| i64eq | i64ne | i64ltu | i64leu | i64gtu | i64geu | i64lts | i64les | i64gts | i64ges
| i64eqz
| i32add | i32sub | i32shl | i32gtu | i32eq
| i32wrap
| i64extu
| select
| loop (body : List WI)
| ite (thn : List WI)
| br (l : Nat)
| callIndirect
| exitTrap (code : Nat)
| exitHalt
/-- Control constructs, calls and terminal pseudo-instructions (handled by `RunI`, not `stepI`). -/
def WI.isCtl : WI β†’ Bool
| .loop _ | .ite _ | .br _ | .callIndirect | .exitTrap _ | .exitHalt => true
| _ => false
def bool32 (b : Bool) : W32 := if b then 1#32 else 0#32
def bin64 (f : W β†’ W β†’ W) (s : WState) : Option WState :=
match s.stack with
| .i64 b :: .i64 a :: r => some { s with stack := .i64 (f a b) :: r }
| _ => none
def cmp64 (f : W β†’ W β†’ Bool) (s : WState) : Option WState :=
match s.stack with
| .i64 b :: .i64 a :: r => some { s with stack := .i32 (bool32 (f a b)) :: r }
| _ => none
def bin32 (f : W32 β†’ W32 β†’ W32) (s : WState) : Option WState :=
match s.stack with
| .i32 b :: .i32 a :: r => some { s with stack := .i32 (f a b) :: r }
| _ => none
/-- One non-control instruction; `none` is a trap. -/
def stepI : WI β†’ WState β†’ Option WState
| .i32const v, s => some { s with stack := .i32 v :: s.stack }
| .i64const v, s => some { s with stack := .i64 v :: s.stack }
| .globalGet i, s => some { s with stack := s.globals i :: s.stack }
| .globalSet i, s =>
match s.stack with
| v :: r => some { s with stack := r, globals := fun j => if j = i then v else s.globals j }
| _ => none
| .i64load off, s =>
match s.stack with
| .i32 a :: r =>
match s.mem.read64 (a.toNat + off) with
| some v => some { s with stack := .i64 v :: r }
| none => none
| _ => none
| .i64store off, s =>
match s.stack with
| .i64 v :: .i32 a :: r =>
match s.mem.write64 (a.toNat + off) v with
| some m => some { s with stack := r, mem := m }
| none => none
| _ => none
| .i64add, s => bin64 (Β· + Β·) s
| .i64sub, s => bin64 (Β· - Β·) s
| .i64mul, s => bin64 (Β· * Β·) s
| .i64and, s => bin64 (Β· &&& Β·) s
| .i64or, s => bin64 (Β· ||| Β·) s
| .i64xor, s => bin64 (Β· ^^^ Β·) s
| .i64shl, s => bin64 (fun a b => a <<< (b.toNat % 64)) s
| .i64shru, s => bin64 (fun a b => a >>> (b.toNat % 64)) s
| .i64rotl, s => bin64 (fun a b => a.rotateLeft (b.toNat % 64)) s
| .i64rotr, s => bin64 (fun a b => a.rotateRight (b.toNat % 64)) s
| .i64divu, s =>
match s.stack with
| .i64 b :: .i64 a :: r =>
if b = 0#64 then none
else some { s with stack := .i64 (BitVec.ofNat 64 (a.toNat / b.toNat)) :: r }
| _ => none
| .i64divs, s =>
match s.stack with
| .i64 b :: .i64 a :: r =>
if b = 0#64 ∨ (a = BitVec.intMin 64 ∧ b = BitVec.allOnes 64) then none
else some { s with stack := .i64 (BitVec.ofInt 64 (Int.tdiv a.toInt b.toInt)) :: r }
| _ => none
| .i64eq, s => cmp64 (fun a b => a == b) s
| .i64ne, s => cmp64 (fun a b => a != b) s
| .i64ltu, s => cmp64 (fun a b => a.ult b) s
| .i64leu, s => cmp64 (fun a b => a.ule b) s
| .i64gtu, s => cmp64 (fun a b => b.ult a) s
| .i64geu, s => cmp64 (fun a b => b.ule a) s
| .i64lts, s => cmp64 (fun a b => a.slt b) s
| .i64les, s => cmp64 (fun a b => a.sle b) s
| .i64gts, s => cmp64 (fun a b => b.slt a) s
| .i64ges, s => cmp64 (fun a b => b.sle a) s
| .i64eqz, s =>
match s.stack with
| .i64 a :: r => some { s with stack := .i32 (bool32 (a == 0#64)) :: r }
| _ => none
| .i32add, s => bin32 (Β· + Β·) s
| .i32sub, s => bin32 (Β· - Β·) s
| .i32shl, s => bin32 (fun a b => a <<< (b.toNat % 32)) s
| .i32gtu, s =>
match s.stack with
| .i32 b :: .i32 a :: r => some { s with stack := .i32 (bool32 (b.ult a)) :: r }
| _ => none
| .i32eq, s =>
match s.stack with
| .i32 b :: .i32 a :: r => some { s with stack := .i32 (bool32 (a == b)) :: r }
| _ => none
| .i32wrap, s =>
match s.stack with
| .i64 a :: r => some { s with stack := .i32 (BitVec.ofNat 32 a.toNat) :: r }
| _ => none
| .i64extu, s =>
match s.stack with
| .i32 a :: r => some { s with stack := .i64 (BitVec.ofNat 64 a.toNat) :: r }
| _ => none
| .select, s =>
match s.stack with
| .i32 c :: v2 :: v1 :: r => some { s with stack := (if c β‰  0#32 then v1 else v2) :: r }
| _ => none
| _, _ => none
/-- Outcomes of running a list of instructions. -/
inductive Res where
| normal (s : WState)
| br (n : Nat) (s : WState)
| exit (code : Nat)
| halted (s : WState)
| fault
/-- Leaving a labelled construct entered with operand stack `entry`: a branch to label 0 is
consumed (for `if`; `loop` re-enters) and the operand stack is unwound to its height at entry;
other branches propagate outward. -/
inductive BlockOut (entry : List Val) : Res β†’ Res β†’ Prop where
| norm {s} : BlockOut entry (.normal s) (.normal s)
| br0 {s} : BlockOut entry (.br 0 s) (.normal { s with stack := entry })
| brS {n s} : BlockOut entry (.br (n + 1) s) (.br n s)
| exit {c} : BlockOut entry (.exit c) (.exit c)
| halted {s} : BlockOut entry (.halted s) (.halted s)
| fault : BlockOut entry .fault .fault
/-- Results that abandon the rest of an instruction sequence. -/
def Res.abrupt : Res β†’ Prop
| .normal _ => False
| _ => True
/-- A function table: entry `i` is the body of function `i`. -/
abbrev Funcs := List (List WI)
mutual
inductive RunI (fs : Funcs) : WI β†’ WState β†’ Res β†’ Prop where
| simple {i s s'} : i.isCtl = false β†’ stepI i s = some s' β†’ RunI fs i s (.normal s')
| simpleFault {i s} : i.isCtl = false β†’ stepI i s = none β†’ RunI fs i s .fault
| loopNormal {body s s'} : Run fs body s (.normal s') β†’ RunI fs (.loop body) s (.normal s')
| loopBr0 {body s s' r} :
Run fs body s (.br 0 s') β†’ RunI fs (.loop body) { s' with stack := s.stack } r β†’
RunI fs (.loop body) s r
| loopBrS {body s n s'} : Run fs body s (.br (n + 1) s') β†’ RunI fs (.loop body) s (.br n s')
| loopExit {body s c} : Run fs body s (.exit c) β†’ RunI fs (.loop body) s (.exit c)
| loopHalted {body s s'} : Run fs body s (.halted s') β†’ RunI fs (.loop body) s (.halted s')
| loopFault {body s} : Run fs body s .fault β†’ RunI fs (.loop body) s .fault
| iteThen {thn s r c res res'} :
s.stack = .i32 c :: r β†’ c β‰  0#32 β†’ Run fs thn { s with stack := r } res β†’
BlockOut r res res' β†’ RunI fs (.ite thn) s res'
| iteElse {thn s r c} :
s.stack = .i32 c :: r β†’ c = 0#32 β†’ RunI fs (.ite thn) s (.normal { s with stack := r })
| iteFault {thn s} : (βˆ€ c r, s.stack β‰  .i32 c :: r) β†’ RunI fs (.ite thn) s .fault
| br {l s} : RunI fs (.br l) s (.br l s)
| callNormal {s r i body s' v} :
s.stack = .i32 i :: r β†’ fs[i.toNat]? = some body β†’
Run fs body { s with stack := [] } (.normal s') β†’ s'.stack = [.i32 v] β†’
RunI fs .callIndirect s (.normal { s' with stack := .i32 v :: r })
| callBad {s r i body s'} :
s.stack = .i32 i :: r β†’ fs[i.toNat]? = some body β†’
Run fs body { s with stack := [] } (.normal s') β†’ (βˆ€ v, s'.stack β‰  [.i32 v]) β†’
RunI fs .callIndirect s .fault
| callAbrupt {s r i body res} :
s.stack = .i32 i :: r β†’ fs[i.toNat]? = some body β†’
Run fs body { s with stack := [] } res β†’ res.abrupt β†’
RunI fs .callIndirect s (match res with | .br _ _ => .fault | other => other)
| callMissing {s r i} :
s.stack = .i32 i :: r β†’ fs[i.toNat]? = none β†’ RunI fs .callIndirect s .fault
| callBadStack {s} : (βˆ€ i r, s.stack β‰  .i32 i :: r) β†’ RunI fs .callIndirect s .fault
| exitTrap {c s} : RunI fs (.exitTrap c) s (.exit c)
| exitHalt {s} : RunI fs .exitHalt s (.halted s)
inductive Run (fs : Funcs) : List WI β†’ WState β†’ Res β†’ Prop where
| nil {s} : Run fs [] s (.normal s)
| consNormal {i is s s' r} : RunI fs i s (.normal s') β†’ Run fs is s' r β†’ Run fs (i :: is) s r
| consAbrupt {i is s r} : RunI fs i s r β†’ r.abrupt β†’ Run fs (i :: is) s r
end
end Wasm
end WordDialect