Skip to content

Commit 182e808

Browse files
mfornetclaude
andcommitted
interpreter: accept folded GC allocator global initializers in the WAT decoder
The global-decl parser detected GC heap-allocating initializers (struct.new / array.new*) by scanning only the top-level atoms of the initializer sexpr, so the folded leaf form (struct.new $s (i32.const 100)) slipped past the check, fell through to the literal-folding shape match, and was rejected with 'global init expression must be i32.const or i64.const'. The same allocation wrapped in extended-const arithmetic was accepted, because the extended-const scan does recurse into folded operands. Generalize the recursive sexpr scan into initExprMentions and use it for both the extended-const detection and a new initExprAllocates predicate, so GC allocators are detected in both the flat form emitted by wasm-tools print and the folded form. Adds regression theorems in Examples/GlobalInitExpr exercising the exact repro from the issue: the leaf and arithmetic-wrapped struct.new initializers both decode and evaluate to a struct whose field reads back as 100. Fixes #109 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3b2e07d commit 182e808

2 files changed

Lines changed: 78 additions & 14 deletions

File tree

interpreter/Interpreter/Wasm/Decoder/Wat.lean

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,18 +2189,33 @@ arithmetic ops (`i32.add`/`i32.sub`/`i32.mul`, i64 ditto). Recurses into
21892189
folded operand forms (the flat form emitted by `wasm-tools print` is
21902190
handled by the list case). -/
21912191
mutual
2192-
/-- Scan a single sexpr; recurse into a `(…)` list via the list helper. -/
2193-
def initExprNeedsEval : Sexpr → Bool
2194-
| .atom a =>
2195-
a == "global.get"
2196-
|| a ∈ #["i32.add", "i32.sub", "i32.mul", "i64.add", "i64.sub", "i64.mul"]
2197-
| .list ys => initExprNeedsEvalList ys
2192+
/-- Scan a single sexpr for an atom satisfying `p`; recurse into a `(…)`
2193+
list via the list helper (covers both the folded operand form and the flat
2194+
form emitted by `wasm-tools print`). -/
2195+
def initExprMentions (p : String → Bool) : Sexpr → Bool
2196+
| .atom a => p a
2197+
| .list ys => initExprMentionsList p ys
21982198
/-- Scan a sequence of sexprs (the operands of a folded instruction). -/
2199-
def initExprNeedsEvalList : List Sexpr → Bool
2199+
def initExprMentionsList (p : String → Bool) : List Sexpr → Bool
22002200
| [] => false
2201-
| y :: ys => initExprNeedsEval y || initExprNeedsEvalList ys
2201+
| y :: ys => initExprMentions p y || initExprMentionsList p ys
22022202
end
22032203

2204+
/-- Scan a single sexpr; recurse into a `(…)` list via the list helper. -/
2205+
def initExprNeedsEval : Sexpr → Bool :=
2206+
initExprMentions fun a =>
2207+
a == "global.get"
2208+
|| a ∈ #["i32.add", "i32.sub", "i32.mul", "i64.add", "i64.sub", "i64.mul"]
2209+
2210+
/-- Whether a global-initializer sexpr mentions a GC heap-allocating constant
2211+
instruction (`struct.new`/`array.new*`), in either the flat or the folded
2212+
form. Such initializers cannot be folded to a literal at decode time. -/
2213+
def initExprAllocates : Sexpr → Bool :=
2214+
initExprMentions fun a =>
2215+
a ∈ #["struct.new", "struct.new_default",
2216+
"array.new", "array.new_default", "array.new_fixed",
2217+
"array.new_data", "array.new_elem"]
2218+
22042219
private def parseGlobalDecl (ctx : Ctx) (xs : List Sexpr) :
22052220
Except Err Wasm.GlobalDecl := do
22062221
let xs := match xs with
@@ -2228,12 +2243,7 @@ private def parseGlobalDecl (ctx : Ctx) (xs : List Sexpr) :
22282243
-- GC heap-allocating initializers (`struct.new`/`array.new*`) cannot be
22292244
-- folded to a single value at decode time; keep the const-expr program so
22302245
-- `Module.runConstGlobals` can evaluate it at instantiation.
2231-
let needsExpr := xs.any fun
2232-
| .atom a => a == "struct.new" || a == "struct.new_default"
2233-
|| a == "array.new" || a == "array.new_default" || a == "array.new_fixed"
2234-
|| a == "array.new_data" || a == "array.new_elem"
2235-
| _ => false
2236-
if needsExpr then
2246+
if xs.any initExprAllocates then
22372247
let prog ← parseInstrSeq ctx xs
22382248
-- The value can't be folded at decode time; stash a placeholder `init`
22392249
-- and let `Module.runConstGlobals` evaluate `initExpr` at instantiation.

interpreter/Interpreter/Wasm/Examples/GlobalInitExpr.lean

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,59 @@ theorem getG_returns_42 :
5555
= [.i32 42] := by
5656
native_decide
5757

58+
/-! ### GC allocator initializers (issue #109)
59+
60+
`struct.new` is a constant instruction, so a global may be initialised
61+
with a plain `(struct.new $s (i32.const 100))`. The decoder used to
62+
detect GC allocators only at the top level of the initializer sexpr, so
63+
the folded leaf form was rejected while the same allocation wrapped in
64+
extended-const arithmetic — caught by the (recursive) extended-const
65+
scan — was accepted. Both forms must decode and evaluate to the same
66+
module behaviour. -/
67+
68+
/-- Leaf form: the initializer is a folded `struct.new` with a plain
69+
`i32.const` field value. -/
70+
def structGlobalLeafWat : String := "
71+
(module
72+
(type $s (struct (field i32)))
73+
(global $g (ref $s) (struct.new $s (i32.const 100)))
74+
(func $f (export \"f\") (result i32)
75+
(struct.get $s 0 (global.get $g))))
76+
"
77+
78+
/-- Arithmetic form: the same allocation with the field value computed by an
79+
extended-const expression. -/
80+
def structGlobalArithWat : String := "
81+
(module
82+
(type $s (struct (field i32)))
83+
(global $g (ref $s) (struct.new $s (i32.add (i32.const 50) (i32.const 50))))
84+
(func $f (export \"f\") (result i32)
85+
(struct.get $s 0 (global.get $g))))
86+
"
87+
88+
private def decodedLeaf : Wasm.Module := decodeOrDefault structGlobalLeafWat
89+
private def decodedArith : Wasm.Module := decodeOrDefault structGlobalArithWat
90+
91+
/-- The leaf `struct.new` initializer decodes (rather than erroring) and is
92+
kept as a const-expr program for `runConstGlobals`. -/
93+
theorem leaf_struct_new_keeps_initExpr :
94+
(decodedLeaf.globals[0]?.map (·.initExpr.isEmpty)).getD true = false := by
95+
native_decide
96+
97+
/-- End-to-end: reading the struct field of the leaf-initialised global
98+
returns `100`. -/
99+
theorem leaf_struct_new_returns_100 :
100+
runValues 64 decodedLeaf 0
101+
(decodedLeaf.runConstGlobals 64 (decodedLeaf.initialStore (α := Unit)) {}) []
102+
= [.i32 100] := by
103+
native_decide
104+
105+
/-- The arithmetic-wrapped form evaluates to the same result. -/
106+
theorem arith_struct_new_returns_100 :
107+
runValues 64 decodedArith 0
108+
(decodedArith.runConstGlobals 64 (decodedArith.initialStore (α := Unit)) {}) []
109+
= [.i32 100] := by
110+
native_decide
111+
58112
end GlobalInitExpr
59113
end Wasm

0 commit comments

Comments
 (0)