plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


5ef4579Peter John 2026-08-10T11:10:35+05:30
feat(plum-wasm-codegen): add wasm-gc type-section emitter (unconsumed) and GC-enabled test Engine
docs/superpowers/plans/2026-07-25-wasm-gc-migration.md ADDED
@@ -0,0 +1,229 @@
1
+ # Wasm-GC Migration Implementation Plan
2
+
3
+ > **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Work task-by-task, in order — later tasks assume earlier ones landed.
4
+
5
+ **Goal:** Replace `plum-wasm-codegen`'s hand-rolled bump-allocator memory model with wasm-gc (`struct`/`array` types, `ref`/`ref null`, `struct.new`, `ref.test`/`br_on_cast`), per `docs/superpowers/specs/2026-07-25-wasm-gc-migration-design.md`. Retire the bump allocator, `MemoryType`, and `bump_global` entirely. Unblocks `libs/std/list.plum`'s `todo` methods (`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`), which is the concrete deliverable.
6
+
7
+ **Architecture:** One wasm-gc `struct` type per concrete (post-monomorphization) class/struct. Per enum, one abstract supertype `struct` plus one concrete subtype `struct` per variant (wasm-gc declared subtyping), matched via `ref.test`/`br_on_cast` — **including `Bool`**, per explicit user decision below. `Str` becomes `array<i8>`. The linear memory, `MemoryType`, and `bump_global` are deleted entirely once nothing depends on them — static string data moves to a **passive** data segment consumed via `array.new_data` (no active memory needed for it at all).
8
+
9
+ **Decisions made during planning that the design spec left open** (read these before starting — they resolve real ambiguities, not stylistic preferences):
10
+
11
+ 1. **Bool is a full wasm-gc struct, no special-casing** (explicit user decision — the alternative, keeping `Bool` as a raw `i32` for cheap control flow, was rejected in favor of literal spec uniformity). Consequence: every `if`/`while` condition, `&&`/`||` operand, and `assert` — anywhere a `Bool` value drives wasm's native control flow (which requires a raw `i32`, not a `ref`) — needs a `ref.test (ref $True)` inserted to convert the `Bool` ref into an `i32` immediately before it's consumed by `Instruction::If`/`BrIf`. Comparison operators (`==`, `!=`, `<`, etc.) go the other direction: the native predicate produces an `i32` 0/1, which then selects between the pre-allocated `$True`/`$False` singletons (see next point) via a small `if/else` yielding a `ref`.
12
+ 2. **Payload-free variants are pre-allocated once as globals, not constructed per-use.** `True`, `False`, `None`, and any other zero-field variant of any enum are immutable and interchangeable, so each gets exactly one `struct.new` at a module-initialization point, stored in a `global`; every subsequent bare reference (`True`, `None`, a comparison result, a discriminant-free `enum` variant) is a `global.get` of that singleton, not a fresh allocation. This avoids needless GC churn for the single highest-frequency value shape in the language (every boolean expression).
13
+ 3. **Closure env pointers are `anyref` in the shared `call_indirect` signature**, not a per-closure concrete type. Every closure literal has a structurally different captured-variable shape, but all closures of the same plum `fn(...)` type must share one wasm function-type for `call_indirect` to dispatch across them uniformly — exactly the role `i32` (an untyped address) plays today. Each closure's own compiled body does `ref.cast (ref $ItsOwnEnvType)` on the incoming `anyref` as its first instruction, recovering its concrete, statically-typed env struct before reading captured fields. The closure value itself (`{table_index, env_pointer}`) becomes a two-field `struct` (`i32`, `anyref`) instead of a bump-allocated pair.
14
+ 4. **String data uses a passive data segment + `array.new_data`**, not an active one — this needs no `memory` section at all, which is what lets the memory section disappear completely rather than sticking around just for string constants.
15
+
16
+ **Tech Stack:** `wasm-encoder` 0.220 (already supports `CoreTypeEncoder::struct_`/`array`/`subtype`/`rec`, `Instruction::StructNew`/`StructGet`/`StructSet`/`ArrayNewData`/`RefTestNonNull`/`BrOnCast`, `RefType`/`HeapType::Concrete` — verified directly against the vendored source, no upgrade needed), `wasmtime` 28 (`Config::wasm_gc`/`wasm_function_references` exist and are reachable — the `"gc"`/`"gc-drc"`/`"gc-null"` cargo features are already in wasmtime's `default` feature set, so no `Cargo.toml` change is needed, just calling the `Config` methods in test setup). **Caveat directly from wasmtime's own doc comment on `Config::wasm_gc`: "Wasmtime's implementation of the GC proposal is still in progress and generally not ready for primetime."** Budget time in Task 1 to discover real limitations empirically, not just from docs.
17
+
18
+ ## Global Constraints
19
+
20
+ - Spec: `docs/superpowers/specs/2026-07-25-wasm-gc-migration-design.md`. This plan's four numbered decisions above fill gaps that spec left unresolved (Bool, closures, singleton variants, string data segment kind) — treat them as binding, not as this plan's own invention to second-guess mid-implementation.
21
+ - This is fundamentally **not** an incrementally-toggleable migration: the moment `Str` (or anything) becomes a `ref`, every class/struct/enum that can hold one transitively can no longer live in linear memory (wasm-gc references cannot be stored in linear memory at all — that's the point of the proposal's separate reference universe). Task 2 is therefore one large, multi-step "big bang" task that switches construction, field/variant access, match dispatch, strings, and closures over together — expect it to be the bulk of this plan's work, and expect the codebase to be non-compiling partway through its steps until the whole task lands. Do not attempt to keep every individual step's diff independently green; the task-level `cargo test --workspace` at the end of Task 2 is the real gate.
22
+ - `plum-checker`'s type-checking/monomorphization needs **no changes** — it already resolves every generic instantiation to a concrete type before codegen runs (confirmed by reading `plum-checker/src/monomorphize.rs`'s existing role in `compileSource`). If you find this assumption wrong, stop and report BLOCKED rather than silently expanding scope into `plum-checker`.
23
+ - Run `cargo build --workspace --all-targets` after every step that touches `plum-wasm-codegen/src/lib.rs` (compilation alone is a fast, cheap checkpoint even mid-task before behavior is testable), and `cargo test --workspace` at the end of every task.
24
+ - `[T]` array literals/indexing and wasm exception-handling remain explicitly out of scope (per the spec) — do not add them opportunistically because GC types make them easier.
25
+
26
+ ---
27
+
28
+ ### Task 1: Wasmtime GC config verification + wasm-gc type-section emitter foundation
29
+
30
+ **Files:**
31
+ - Modify: `plum-wasm-codegen/Cargo.toml` (only if the empirical check in Step 1 finds the default features insufficient)
32
+ - Modify: `plum-wasm-codegen/tests/codegen_tests.rs`, `plum-wasm-codegen/tests/examples_test.rs`
33
+ - Modify: `plum-wasm-codegen/src/lib.rs`
34
+
35
+ **Interfaces:**
36
+ - Consumes: nothing (bottom of this plan's dependency chain).
37
+ - Produces: every `wasmtime::Engine`/`Config` used in tests has the GC proposal enabled; a new type-section emitter that, given the checker's monomorphized `Source`, emits one wasm-gc `struct` per concrete class, one abstract-supertype-plus-per-variant-subtype set per enum (Bool included, built in exactly like the existing hardcoded `True`/`False` `EnumVariantInfo` registration), and one `array<i8>` type for `Str` — **wired up but not yet consumed by any real codegen path**, so all EXISTING tests (still running through the untouched bump-allocator codegen) stay green. This task is purely additive.
38
+
39
+ - [ ] **Step 1: Confirm wasmtime's GC `Config` actually works empirically, first**
40
+
41
+ Before writing any type-emitter code, write a throwaway test (in `codegen_tests.rs`, can be deleted/replaced by Task 2) that: builds a `wasmtime::Config`, calls `.wasm_gc(true)` (and `.wasm_function_references(true)` — check by reading `wasmtime-28.0.1`'s `config.rs`, vendored at `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasmtime-28.0.1/src/config.rs`, whether GC has an undeclared dependency on function-references that isn't auto-enabled), constructs an `Engine` from it, and hand-encodes (via `wasm_encoder` directly, no plum involved) the smallest possible module with one GC struct type, one function that does `struct.new_default` + returns it typed as `(ref null $T)`, and confirms it validates AND instantiates+runs under that `Engine`. If this fails or exhibits a real engine limitation, that finding drives every subsequent step — report it before proceeding, don't route around it silently.
42
+
43
+ - [ ] **Step 2: Update the test harness's `Engine`/`Config` construction**
44
+
45
+ Read the current `Engine::default()` (or equivalent) call sites in `codegen_tests.rs` and `examples_test.rs` (search `wasmtime::Engine`) and replace with the `Config` confirmed working in Step 1. Every test in both files must still pass unchanged (they're still exercising the untouched bump-allocator codegen at this point) — this step is purely "can a GC-enabled engine still run today's non-GC output," which it should, since enabling a feature doesn't retroactively require using it.
46
+
47
+ - [ ] **Step 3: Read the current type-registration surface before extending it**
48
+
49
+ Read `plum-checker`'s `ClassEnv`/`EnumVariants`/`EnumVariantInfo` definitions (`plum-checker/src/lib.rs`) and `plum-wasm-codegen`'s `CompileCtx`/`LocalCtx` (`plum-wasm-codegen/src/lib.rs`) exact current fields — this plan's earlier research read them as of the enum-discriminant-values plan's completion, but re-verify, since intervening work may have touched them.
50
+
51
+ - [ ] **Step 4: Add a GC type registry, populated but unconsumed**
52
+
53
+ Add a new struct (e.g. `GcTypeRegistry`) that, given the monomorphized `ast::Source` plus the existing `ClassEnv`/`EnumVariants` tables, assigns and records a wasm type-section index for: every concrete class name, every enum's abstract supertype, every enum variant's concrete subtype, and one shared `array<i8>` index for `Str`. Emit these into a real `TypeSection` via `CoreTypeEncoder::struct_`/`array`/`subtype` (structs need `SubType { is_final: false, supertype_idx: None, ... }` for a class since it needs no subtyping, but an enum's supertype needs `is_final: false` so variants CAN subtype it, and each variant subtype needs `SubType { supertype_idx: Some(super_idx), ... }`) — group the whole enum's supertype+variants in one `rec` group (`CoreTypeEncoder::rec`) since wasm-gc subtyping requires supertype and subtype to be declared in the same recursive group or supertype declared earlier in an earlier group; read `wasm-encoder`'s `rec`/`subtype` doc comments (`~/.cargo/registry/.../wasm-encoder-0.220.1/src/core/types.rs`) to get the ordering constraint exactly right. Thread the resulting index tables through `CompileCtx` as new fields (e.g. `class_type_idx: HashMap<String, u32>`, `enum_super_type_idx: HashMap<String, u32>`, `variant_type_idx: HashMap<String, u32>`, `str_type_idx: u32`), populated in `compileSource` but not read by anything else yet.
54
+
55
+ - [ ] **Step 5: Add a corresponding Rust test proving the type section is well-formed**
56
+
57
+ Add a test that compiles a small source (one class, one enum with a payload variant and a payload-free variant) through `compileSource`, and asserts the resulting bytes validate via `wasmparser::validate` under the GC-enabled config from Step 1 — this proves the new type section alone (still unused by real codegen) is well-formed, before Task 2 starts depending on it.
58
+
59
+ - [ ] **Step 6: Run full verification**
60
+
61
+ `cargo build --workspace --all-targets` then `cargo test --workspace`. Expected: 100% pass, identical results to before this task — nothing behavioral changed yet.
62
+
63
+ - [ ] **Step 7: Commit**
64
+
65
+ ```bash
66
+ git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs plum-wasm-codegen/tests/examples_test.rs plum-wasm-codegen/Cargo.toml
67
+ git commit -m "feat(plum-wasm-codegen): add wasm-gc type-section emitter (unconsumed) and GC-enabled test Engine"
68
+ ```
69
+
70
+ ---
71
+
72
+ ### Task 2: The big switchover — construction, field/variant access, match dispatch, strings, and closures onto wasm-gc
73
+
74
+ **Files:** `plum-wasm-codegen/src/lib.rs` (the overwhelming majority of this task), `plum-wasm-codegen/tests/codegen_tests.rs`, `plum-wasm-codegen/tests/examples_test.rs`
75
+
76
+ **Interfaces:**
77
+ - Consumes: the type registry from Task 1.
78
+ - Produces: every codegen path that currently reads/writes `ctx.bump_global` instead does `struct.new`/`struct.get`/`struct.set`/`array.*`; `match` dispatch uses `ref.test`/`br_on_cast`; the memory section, `bump_global`, `HEAP_BASE`, `STRING_BASE` are deleted with nothing left depending on them.
79
+
80
+ This task is large enough that its steps are grouped by concern. Within each concern, **read the current exact code first** — this plan describes shapes and gives file:line references from the research pass that wrote it, but by the time you reach a given step, earlier steps in this same task will have already changed surrounding code. Re-verify against the live file, not this plan's prose.
81
+
82
+ #### 2a. Bool/enum singletons and control-flow conversion (do this first — nearly everything else depends on being able to produce/consume a Bool)
83
+
84
+ - [ ] **Step 1: Read `compileSource`'s current initialization sequence, `compileStmt`'s `If`/`While` arms, `boolean_operator`/comparison codegen, and `assert` codegen**
85
+
86
+ These are all in `plum-wasm-codegen/src/lib.rs` — find them by searching `ast::Stmt::If`, `ast::Stmt::While`, `ast::Expr::Bool(`, `ast::Expr::Compare(`, `ast::Stmt::Assert`.
87
+
88
+ - [ ] **Step 2: Emit one global per payload-free variant across the whole program (including `True`/`False`)**
89
+
90
+ In `compileSource`, after the type registry (Task 1) is built, walk every enum's variants (plus the built-in `Bool`) and for each payload-free one, emit a `global` (mutable: false is ideal but wasm-gc struct globals may need an initializer expression capable of `struct.new` — read `wasm_encoder`'s `ConstExpr`/`GlobalSection` to confirm whether a GC `struct.new` is legal inside a global's constant-initializer expression under this wasm-gc revision; if not, fall back to a `start` function that runs once and stores each singleton into a mutable global before `main`/any export can run). Record `variant_name -> global_index` in a new `CompileCtx` field.
91
+
92
+ - [ ] **Step 3: Rewrite conditional control flow to convert Bool refs to i32 via `ref.test`**
93
+
94
+ Wherever a `Bool`-typed value currently feeds `Instruction::If`/`BrIf` unchanged (it's already an i32 today), insert `Instruction::RefTestNonNull(HeapType::Concrete(true_variant_type_idx))` immediately before it. This applies to at least: `if`/`else if` conditions, `while` conditions, `assert`, and short-circuit `&&`/`||` (which currently, per existing codegen, likely already lower to nested `If` blocks for short-circuiting — read the current `boolean_operator` codegen to confirm the exact shape before changing it).
95
+
96
+ - [ ] **Step 4: Rewrite comparison operators to produce a Bool ref from a native i32 predicate**
97
+
98
+ Currently a comparison (`==`, `<`, etc.) directly leaves an i32 on the stack as the Bool result. Change it to: compute the native i32 predicate as today, then `Instruction::If(BlockType::Result(bool_ref_type))` / push `GlobalGet(true_singleton)` / `Else` / `GlobalGet(false_singleton)` / `End`, so the result is a proper `Bool` ref matching every other Bool-producing expression.
99
+
100
+ - [ ] **Step 5: Update every OTHER payload-free bare-variant construction site to use its singleton global instead of allocating**
101
+
102
+ Search for the current handling of `ast::Expr::TypeName(n)` where `info.field_types.is_empty()` (bare tag construction) — this currently does `Instruction::I32Const(info.tag)`; change it to `Instruction::GlobalGet(singleton_idx)`.
103
+
104
+ - [ ] **Step 6: Build and run affected tests only, iterating**
105
+
106
+ `cargo build -p plum-wasm-codegen` first (fast compile-only check — expect many errors from downstream code still assuming i32 Bool/tags, which is fine, that's 2b/2c/2d's job). Do not attempt `cargo test` yet — the crate won't fully compile again until struct/enum construction (2b) and match dispatch (2d) also land, since they're mutually dependent on the same representation.
107
+
108
+ #### 2b. Struct/class construction and field access
109
+
110
+ - [ ] **Step 1: Read the current `ast::Expr::ClassCall` construction arm and the `Attribute`/`Field` read/write codegen**
111
+
112
+ Search `ast::Expr::ClassCall`, `ast::AttrKind::Field`, `ast::AssignTarget::Field`.
113
+
114
+ - [ ] **Step 2: Rewrite construction to `struct.new`**
115
+
116
+ For each `ClassCall`, look up the class's declared field order (not `call.fields`'s written order — construction must push values in DECLARATION order for `struct.new`). For each declared field, find the matching `call.fields` entry by name and `compileExpr` its value onto the stack; once every field's value is pushed, emit `Instruction::StructNew(class_type_idx)`. The `classcall_scratch` local is very likely no longer needed for this case (no bump-pointer bookkeeping requires holding an intermediate address — verify by reading whether anything else in the current code relies on that scratch slot for `ClassCall` specifically before deleting its allocation for this case).
117
+
118
+ - [ ] **Step 3: Rewrite field reads to `struct.get`**
119
+
120
+ Replace the current class-field-read arm's `emitLoad` at a byte offset with `compileExpr` (pushes the object ref) followed by `Instruction::StructGet { struct_type_index: class_type_idx, field_index }`.
121
+
122
+ - [ ] **Step 4: Rewrite field writes (`self.x = ...`, chained `a.b.c = ...`) to `struct.set`**
123
+
124
+ Same shape: compile the object ref, compile the new value, `Instruction::StructSet { struct_type_index, field_index }`. `StructSet`'s stack order — verify from `wasm-encoder`'s encode impl or the wasm-gc spec whether it expects `(struct_ref, value)` or `(value, struct_ref)` push order; get this from the actual instruction encoding, not assumption, since getting it backwards produces a type-valid-looking-in-isolation-but-wrong-at-runtime bug that validation may not always catch depending on other stack contents.
125
+
126
+ - [ ] **Step 5: Rewrite discriminant-enum field access (`self.n` on an `enum Foo(n: Int)`)** analogously, through `ctx.enum_params`, using `StructGet`/`StructSet` against the enum's own struct layout with the `field_idx + 1` tag-slot offset convention preserved conceptually (though the "+1 for tag" byte-offset trick is a linear-memory artifact — under wasm-gc each variant subtype simply has its own explicit field list starting at field index 0 for the first real field, no tag slot needed at all, since the tag IS the type itself now, discoverable via `ref.test`, not a stored value read at a fixed offset).
127
+
128
+ #### 2c. String migration to `array<i8>`
129
+
130
+ - [ ] **Step 1: Read the current string literal encoding, `registerStringConcatHelper`, `registerIntToStringHelper`, and string interpolation codegen**
131
+
132
+ Search `compile_static_string` (or equivalent), `registerStringConcatHelper`, `registerIntToStringHelper`.
133
+
134
+ - [ ] **Step 2: Static string literals become passive-data-segment `array.new_data`**
135
+
136
+ Each string literal's bytes go into one passive `DataSection::passive(bytes)` segment (no active memory offset — confirm `wasm_encoder`'s `DataSection::passive` doesn't require a preceding memory section to exist at all, per this plan's Decision 4). At the literal's use site, emit `Instruction::ArrayNewData { array_type_index: str_type_idx, array_data_index }`.
137
+
138
+ - [ ] **Step 3: Rewrite `registerStringConcatHelper` against `array.new`/`array.copy`**
139
+
140
+ Read the exact current helper body first. The new version needs the combined length (`array.len` on both inputs, or track lengths already known from a length-prefix design if you keep one — decide whether `Str`'s `array<i8>` needs an explicit stored length or relies on `array.len`, and note that GC arrays DO track their own length natively, so a manual length prefix is no longer needed at all, simplifying this over the current length-prefixed-blob design), allocates a new array of that combined size (`array.new_default` sized dynamically — check whether `array.new_default` accepts a non-constant size operand; it does, per the wasm-gc spec, size is a runtime i32 operand), then two `Instruction::ArrayCopy { array_type_index_dst, array_type_index_src }` calls to copy each source's bytes into the destination at the right offset.
141
+
142
+ - [ ] **Step 4: Rewrite `registerIntToStringHelper` similarly** — read its current byte-writing loop and adapt each byte store to `array.set` (or build into a fixed-size scratch and `array.new_data`-style copy, whichever the current algorithm's shape most directly maps to).
143
+
144
+ - [ ] **Step 5: Update string interpolation codegen** to call the rewritten helpers; verify `Str`-typed class/enum fields (now `ref null $StrArray` instead of i32) round-trip correctly through 2b/2d's `struct.get`/`struct.set` and `ref.test`/variant-subtype field lists.
145
+
146
+ #### 2d. Match dispatch via `ref.test`/`br_on_cast`
147
+
148
+ - [ ] **Step 1: Read `compileMatch`, `compileMatchArmsMulti`, `compileCasePositions`, `compileVariantEqArm` (or its current name — it may have been renamed during the camelCase pass), `compileVariantConstructorArm`, and `compileFieldPatterns` in full before changing any of them** — this is the most intricate part of the existing codegen (recursive multi-subject, multi-position, nested-constructor-pattern dispatch) and the part most likely to have subtle behavioral requirements not obvious from a partial read.
149
+
150
+ - [ ] **Step 2: Replace the `HEAP_BASE` range-check + tag-load-and-compare pattern with `ref.test`**
151
+
152
+ Every place that currently does "range-check against `HEAP_BASE`, then conditionally load+compare a tag" (both the top-level bare-Name/Class-pattern arms and the nested `compileFieldPatterns` equivalents) becomes a single `Instruction::RefTestNonNull(HeapType::Concrete(variant_type_idx))` (or `RefTestNullable` if the value can legitimately be null in context — decide per call site) directly on the subject ref, producing the same "did this match" i32 the existing `If`/`Else` structure around it already expects — the surrounding control-flow shape (fall through to `compileMatchArmsMulti(rest, ...)` on mismatch) doesn't need to change, only how the boolean test itself is computed.
153
+
154
+ - [ ] **Step 3: Replace payload-field extraction with `ref.cast` + `struct.get`**
155
+
156
+ Where a matched constructor pattern currently loads a field via `emitLoad(field_vt, offset)` from the raw pointer, first `Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))` (narrowing the statically-typed-as-supertype subject to its concrete variant subtype — the match arm's binding scope is exactly where this narrowing is valid) then `Instruction::StructGet { struct_type_index: variant_type_idx, field_index }`.
157
+
158
+ - [ ] **Step 4: Consider (but don't require) using `br_on_cast` for the top-level dispatch chain** — the spec's Components section suggests `ref.test`/`br_on_cast` as the replacement mechanism; a straightforward `ref.test` + `if/else` chain (mirroring today's control-flow shape exactly, per Step 2) is very likely simpler to get right than restructuring into `br_on_cast`'s branch-table-like shape, and is equally correct. Only reach for `br_on_cast` if the `if/else` chain form turns out to hit a real wasm-gc validation or structural limitation — don't restructure working control flow for its own sake.
159
+
160
+ #### 2e. Closures
161
+
162
+ - [ ] **Step 1: Read `compileClosureLiteral`, `compileClosureCall`, `compileClosureBody`, and the `Collector`'s closure-discovery pass in full.**
163
+
164
+ - [ ] **Step 2: Give every closure literal its own env `struct` type**, one field per captured variable in `free_vars` order, field types matching each captured variable's own type (which may itself now be a `ref` type post-2b/2c). Register these in the type registry alongside classes/enums.
165
+
166
+ - [ ] **Step 3: Rewrite `compileClosureLiteral`**: build the env via `struct.new` (pushing each captured local's current value, per Decision 3 — snapshot-by-value semantics are unchanged, only the allocation mechanism changes), then build the 2-field closure struct `{i32 table_index, anyref env}` via `struct.new`, casting/widening the concrete env ref to `anyref` for storage (a supertype widening — confirm whether `wasm-encoder`/wasm-gc requires an explicit instruction for this or whether it's implicit at the type-checking level when a subtype value is used where a supertype/`anyref` is expected; if implicit, no extra instruction is needed).
167
+
168
+ - [ ] **Step 4: Rewrite `compileClosureBody`**: its first instructions must `RefCastNonNull` (or the nullable variant, matching the declared param type) the incoming `anyref` env parameter down to `(ref $ThisClosuresEnvType)` before any captured-field reads, then use `struct.get` for each.
169
+
170
+ - [ ] **Step 5: Update the shared `call_indirect` function-type signatures** (`fnParamTypeToWasmSig`/`ClosureSigKey` and wherever `call_indirect` types are registered/deduped) so the env-pointer parameter position is `ValType::Ref(RefType::ANYREF)` instead of `ValType::I32`, everywhere this shape is constructed.
171
+
172
+ #### 2f. Retire the bump allocator entirely
173
+
174
+ - [ ] **Step 1: Remove `HEAP_BASE`, `STRING_BASE`, `bump_global` (the field, its threading through `CompileCtx`/`LocalCtx`, and its initialization in `compileSource`), and the `addMemory`/memory-section code in `WasmModule::finish`** once `cargo build -p plum-wasm-codegen` shows nothing references them anymore. If anything still does, that's a sign an earlier 2a–2e step was missed or incomplete — go back, don't leave a partial bump-allocator fallback path.
175
+ - [ ] **Step 2: Remove now-dead scratch-slot bookkeeping** (`classcall_scratch` etc., in `Collector` and `LocalCtx`) for any use case fully replaced by direct stack-order `struct.new` construction (per 2b Step 2) — but only what's actually provably dead; `match`'s subject-holding scratch locals are still needed (now typed as `ref null` instead of `i32`), don't remove those.
176
+
177
+ #### 2g. Full verification for Task 2
178
+
179
+ - [ ] **Step 1: `cargo build --workspace --all-targets`** — iterate until clean.
180
+ - [ ] **Step 2: Update every existing `codegen_tests.rs`/`examples_test.rs` test that encoded assumptions about the old representation** (e.g. anything asserting raw byte layout, or relying on `HEAP_BASE`-adjacent behavior) — re-verify each still asserts the right OBSERVABLE behavior (a program's computed result), not the old mechanism.
181
+ - [ ] **Step 3: `cargo test --workspace`** — expected: 100% pass. This is the real gate for this entire task; do not commit Task 2 before this is green.
182
+ - [ ] **Step 4: Commit**
183
+
184
+ ```bash
185
+ git add plum-wasm-codegen/src plum-wasm-codegen/tests
186
+ git commit -m "feat(plum-wasm-codegen): migrate structs, enums, strings, and closures to wasm-gc"
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Task 3: Implement `List`'s `todo` methods against the new struct representation
192
+
193
+ **Files:** `libs/std/list.plum`, `plum-wasm-codegen/tests/codegen_tests.rs`
194
+
195
+ **Interfaces:**
196
+ - Consumes: `struct.new`/`struct.set`/`ref null` field semantics from Task 2 — specifically, that unlinking a `Node` (overwriting a `next`/`prev` field with `None`) makes it unreachable and GC-reclaimed with no explicit free.
197
+ - Produces: `add`, `set`, `removeAt`, `remove`, `clear`, `reverse` on `List[T]` (currently `todo`) have real implementations; `Map` (built on `List`) works transitively.
198
+
199
+ - [ ] **Step 1: Read `libs/std/list.plum`'s current `Node`/`List` field declarations and the already-implemented `get`/`length`/`each`/`map` methods** for the established style (`Option[Node]` traversal via `match current`) to match.
200
+ - [ ] **Step 2: Implement `add`** — append (or prepend, per the existing doc comment on `add` — re-read it; the comment currently says "adds the specified elements to the start of the list" — confirm this is the intended semantic, since "add" more commonly means append, and fix the comment or the implementation, whichever is actually wrong, rather than silently implementing whichever is more convenient) by splicing a new `Node` in via `self.tail`/`self.head` and updating `size`.
201
+ - [ ] **Step 3: Implement `set`** — traverse to index `i` (mirroring `get`'s traversal), overwrite the found node's `value` field, return `Some(oldValue)` or `None` if out of bounds.
202
+ - [ ] **Step 4: Implement `removeAt`/`remove`** — traverse to the target node, splice it out by rewriting its neighbors' `prev`/`next` to point at each other (or to `None` at the list's ends, updating `self.head`/`self.tail` accordingly), decrement `size`. The removed node becomes unreachable once nothing points to it — no manual free needed.
203
+ - [ ] **Step 5: Implement `clear`** — reset `self.head`/`self.tail` to `None` and `size` to `0`; every node becomes unreachable transitively.
204
+ - [ ] **Step 6: Implement `reverse`** — build (or in-place relink) a new list with `prev`/`next` swapped at every node.
205
+ - [ ] **Step 7: Add codegen tests** in `plum-wasm-codegen/tests/codegen_tests.rs` for each newly-implemented method, plus one test specifically proving a `Node.next: Option[Node]` field correctly becomes unreachable after `removeAt`/`clear` (per the spec's Testing section) — this can only be an indirect proof (e.g. removing many nodes in a loop and confirming the program still runs correctly and the list's `length`/traversal reflects the removal), since there's no direct "assert this was garbage collected" hook available from a compiled program's own execution.
206
+ - [ ] **Step 8: Run `cargo test --workspace`.** Expected: 100% pass, including new tests.
207
+ - [ ] **Step 9: Commit**
208
+
209
+ ```bash
210
+ git add libs/std/list.plum plum-wasm-codegen/tests/codegen_tests.rs
211
+ git commit -m "feat(libs/std): implement List's add/set/removeAt/remove/clear/reverse on wasm-gc"
212
+ ```
213
+
214
+ ---
215
+
216
+ ### Task 4: Final verification and README update
217
+
218
+ **Files:** `README.md` (Known Gaps section)
219
+
220
+ - [ ] **Step 1: `cargo build --workspace --all-targets` and `cargo test --workspace`.** Expected: 100% pass.
221
+ - [ ] **Step 2: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test`** — unaffected by this plan (no grammar changes), confirm it's still 100% as a sanity check that nothing unrelated regressed.
222
+ - [ ] **Step 3: Run `scripts/test-examples.sh`** (added in an earlier session) to confirm every `examples/*.plum` file still compiles and runs correctly through the real CLI end to end, not just the unit test suite.
223
+ - [ ] **Step 4: Update README's "Known gaps" section** — the current entry "`libs/std`'s actual `List`/`Map` still don't fully compile ... `List`'s methods beyond `get`/`length` ... are still `todo`" is resolved by Task 3; remove or rewrite that bullet to reflect reality. Check whether `Map`'s own gap (references `Buffer`/`Stringable` trait-bounded dispatch, which `plum-checker` still doesn't process) is still accurate and leave that part as-is if so — don't overclaim `Map` fully works if trait dispatch still doesn't exist.
224
+ - [ ] **Step 5: Commit**
225
+
226
+ ```bash
227
+ git add README.md
228
+ git commit -m "docs: update Known gaps for wasm-gc migration and List's new methods"
229
+ ```
plum-wasm-codegen/src/lib.rs CHANGED
@@ -15,8 +15,25 @@ const HEAP_BASE: u32 = 65536;
15
15
  /// String literal data (length-prefixed UTF-8 blobs) is laid out from here upward.
16
16
  const STRING_BASE: u32 = 8;
17
17
 
18
+ /// One entry in the module's type section. Wasm's type section is a SINGLE shared
19
+ /// index space for function types AND (once wasm-gc is in play) composite
20
+ /// struct/array types — `Rec` entries occupy as many consecutive indices as they
21
+ /// have members, exactly like `CoreTypeEncoder::rec` groups multiple sub-types
22
+ /// under one recursive-group declaration.
23
+ enum TypeEntry {
24
+ Func(FuncType),
25
+ /// A whole `rec` group of struct/array sub-types, declared together so members
26
+ /// can reference each other (including themselves) regardless of declaration
27
+ /// order within the group.
28
+ Rec(Vec<SubType>),
29
+ }
30
+
18
31
  pub struct WasmModule {
19
- types: Vec<FuncType>,
32
+ types: Vec<TypeEntry>,
33
+ /// Running count of type-section INDICES assigned so far — NOT the same as
34
+ /// `types.len()`, since one `TypeEntry::Rec` occupies as many indices as it has
35
+ /// members while still being a single `Vec` element.
36
+ next_type_idx: u32,
20
37
  imports: Vec<(String, String, u32)>,
21
38
  functions: Vec<(u32, Vec<u8>)>,
22
39
  exports: Vec<(String, ExportKind, u32)>,
@@ -35,6 +52,7 @@ impl WasmModule {
35
52
  pub fn new() -> Self {
36
53
  Self {
37
54
  types: Vec::new(),
55
+ next_type_idx: 0,
38
56
  imports: Vec::new(),
39
57
  functions: Vec::new(),
40
58
  exports: Vec::new(),
@@ -49,11 +67,26 @@ impl WasmModule {
49
67
  }
50
68
 
51
69
  pub fn addType(&mut self, params: &[ValType], results: &[ValType]) -> u32 {
52
- let idx = self.types.len() as u32;
70
+ let idx = self.next_type_idx;
53
- self.types.push(FuncType::new(params.iter().copied(), results.iter().copied()));
71
+ self.types.push(TypeEntry::Func(FuncType::new(params.iter().copied(), results.iter().copied())));
72
+ self.next_type_idx += 1;
54
73
  idx
55
74
  }
56
75
 
76
+ /// Declares a whole `rec` group of wasm-gc struct/array sub-types together,
77
+ /// returning the type index assigned to each member, in order. Grouping
78
+ /// unrelated types is harmless — the point is that MUTUALLY referencing types
79
+ /// (e.g. an enum's supertype and its variant subtypes, or a self-referential
80
+ /// struct field) MUST share a `rec` group to reference each other regardless of
81
+ /// which one is declared "first".
82
+ pub fn addGcTypes(&mut self, subtypes: Vec<SubType>) -> Vec<u32> {
83
+ let base = self.next_type_idx;
84
+ let count = subtypes.len() as u32;
85
+ self.types.push(TypeEntry::Rec(subtypes));
86
+ self.next_type_idx += count;
87
+ (base..base + count).collect()
88
+ }
89
+
57
90
  pub fn addImport(&mut self, module: &str, name: &str, type_idx: u32) -> u32 {
58
91
  let idx = self.func_import_count;
59
92
  self.imports.push((module.to_string(), name.to_string(), type_idx));
@@ -108,8 +141,15 @@ impl WasmModule {
108
141
 
109
142
  // Type section
110
143
  let mut types = TypeSection::new();
111
- for ft in &self.types {
144
+ for entry in &self.types {
145
+ match entry {
146
+ TypeEntry::Func(ft) => {
112
- types.ty().function(ft.params().iter().copied(), ft.results().iter().copied());
147
+ types.ty().function(ft.params().iter().copied(), ft.results().iter().copied());
148
+ }
149
+ TypeEntry::Rec(subtypes) => {
150
+ types.ty().rec(subtypes.iter().cloned());
151
+ }
152
+ }
113
153
  }
114
154
  module.section(&types);
115
155
 
@@ -274,6 +314,11 @@ pub struct CompileCtx<'a> {
274
314
  /// Shared runtime helper `(n: i64) -> i32`: bump-allocates a new length-prefixed
275
315
  /// string holding `n`'s decimal representation, for interpolating an `Int`.
276
316
  pub int_to_string_func: u32,
317
+ /// wasm-gc type-section indices for this program's classes/enums/Str (Task 1 of
318
+ /// docs/superpowers/plans/2026-07-25-wasm-gc-migration.md) — populated but not
319
+ /// yet consumed by any codegen below; the bump allocator remains the real
320
+ /// representation until Task 2 lands.
321
+ pub gc_types: GcTypeRegistry,
277
322
  }
278
323
 
279
324
  /// Per-module state that accumulates as function bodies are compiled: the running
@@ -377,6 +422,170 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
377
422
  }
378
423
  }
379
424
 
425
+ /// Type-section indices for every wasm-gc composite type this program's monomorphized
426
+ /// classes/enums/`Str` need. Populated once in `compileSource` from the checker's
427
+ /// global tables. Per Task 1 of docs/superpowers/plans/2026-07-25-wasm-gc-migration.md,
428
+ /// this is currently populated but NOT YET consumed by construction/field-access/match
429
+ /// codegen (that's Task 2) — its job right now is only to prove the type section itself
430
+ /// is well-formed under the new representation, alongside the still-untouched
431
+ /// bump-allocator codegen path.
432
+ pub struct GcTypeRegistry {
433
+ /// Concrete class/struct name -> its one wasm-gc `struct` type index.
434
+ pub class_type_idx: HashMap<String, u32>,
435
+ /// Enum name -> its abstract supertype `struct` type index (every variant
436
+ /// subtypes this) — includes the built-in `Bool` enum, which has no
437
+ /// `ast::Item::Enum` of its own (`buildGlobalTables` hardcodes its
438
+ /// `True`/`False` variants directly into `EnumVariants`).
439
+ pub enum_super_type_idx: HashMap<String, u32>,
440
+ /// Variant name (flat namespace, matching `EnumVariants`) -> its concrete
441
+ /// subtype `struct` type index.
442
+ pub variant_type_idx: HashMap<String, u32>,
443
+ /// The single shared `array<i8>` type index every `Str` value uses.
444
+ pub str_type_idx: u32,
445
+ }
446
+
447
+ /// Resolves a plum type to the wasm-gc `ValType` its values are represented as, given
448
+ /// an ALREADY fully-populated `GcTypeRegistry` — every class/enum/Str index must exist
449
+ /// before this is called, since e.g. a class field of another class's type needs that
450
+ /// other class's index to already be assigned (see `buildGcTypeRegistry`'s two-pass
451
+ /// structure: this function is only ever called during its second pass).
452
+ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
453
+ match t {
454
+ PlumType::TInt => ValType::I64,
455
+ PlumType::TFloat => ValType::F64,
456
+ PlumType::TBool => {
457
+ let idx = *registry.enum_super_type_idx.get("Bool").expect("internal codegen error: Bool must be registered in the GC type registry");
458
+ ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(idx) })
459
+ }
460
+ PlumType::TStr => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
461
+ PlumType::TNamed(name) => {
462
+ let idx = registry.class_type_idx.get(name)
463
+ .or_else(|| registry.enum_super_type_idx.get(name))
464
+ .unwrap_or_else(|| panic!("internal codegen error: unknown type '{}' in GC type registry", name));
465
+ ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) })
466
+ }
467
+ // TFun (closures) and TVariadic get their own concrete representation once
468
+ // Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
469
+ // yet-meaningful placeholder in the meantime, since nothing consumes it yet.
470
+ PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
471
+ PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
472
+ }
473
+ }
474
+
475
+ /// Builds the wasm-gc type registry for every concrete class/enum in `source`, plus
476
+ /// the built-in `Bool` enum and the shared `Str` array type, and declares them all as
477
+ /// ONE `rec` group via `module.addGcTypes` — a single group sidesteps every ordering
478
+ /// question about mutual/self-references (a class field of another class's type, an
479
+ /// enum variant field referencing its own enum, `Node.next: Option[Node]`, etc.),
480
+ /// since within one `rec` group members may reference each other regardless of
481
+ /// declaration order.
482
+ fn buildGcTypeRegistry(
483
+ module: &mut WasmModule,
484
+ source: &ast::Source,
485
+ classes: &ClassEnv,
486
+ enum_variants: &EnumVariants,
487
+ ) -> GcTypeRegistry {
488
+ enum Slot {
489
+ Str,
490
+ Class(String),
491
+ EnumSuper,
492
+ Variant(String),
493
+ }
494
+
495
+ // Pass 1: assign every entry a slot (and therefore a type index) up front, before
496
+ // any field list is built, so field-type resolution can reference ANY other entry.
497
+ let mut slots: Vec<Slot> = vec![Slot::Str];
498
+ let mut class_type_idx: HashMap<String, u32> = HashMap::new();
499
+ let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
500
+ let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
501
+
502
+ for item in &source.items {
503
+ if let ast::Item::Class(c) = item {
504
+ class_type_idx.insert(c.name.clone(), slots.len() as u32);
505
+ slots.push(Slot::Class(c.name.clone()));
506
+ }
507
+ }
508
+
509
+ // Bool is built into `EnumVariants` (True/False) by `buildGlobalTables` with no
510
+ // `ast::Item::Enum` of its own (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`'s
511
+ // Decision 1: Bool is a full wasm-gc struct, no special-casing) — register it
512
+ // exactly like a real enum here, ahead of whatever the source actually declares.
513
+ let mut enum_decls: Vec<(String, Vec<String>)> =
514
+ vec![("Bool".to_string(), vec!["False".to_string(), "True".to_string()])];
515
+ for item in &source.items {
516
+ if let ast::Item::Enum(e) = item {
517
+ enum_decls.push((e.name.clone(), e.variants.iter().map(|v| v.name.clone()).collect()));
518
+ }
519
+ }
520
+ for (enum_name, variant_names) in &enum_decls {
521
+ enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
522
+ slots.push(Slot::EnumSuper);
523
+ for vname in variant_names {
524
+ variant_type_idx.insert(vname.clone(), slots.len() as u32);
525
+ slots.push(Slot::Variant(vname.clone()));
526
+ }
527
+ }
528
+
529
+ // The registry is fully index-complete after pass 1 (every name has an assigned
530
+ // slot) even though no field lists exist yet — safe to hand to `plumTypeToGcValtype`
531
+ // for pass 2's field-type resolution.
532
+ let registry = GcTypeRegistry {
533
+ class_type_idx,
534
+ enum_super_type_idx,
535
+ variant_type_idx,
536
+ str_type_idx: 0,
537
+ };
538
+
539
+ // Pass 2: build the real SubType for every slot, now that every cross-reference
540
+ // resolves.
541
+ let subtypes: Vec<SubType> = slots.iter().map(|slot| match slot {
542
+ Slot::Str => SubType {
543
+ is_final: true,
544
+ supertype_idx: None,
545
+ composite_type: CompositeType {
546
+ inner: CompositeInnerType::Array(ArrayType(FieldType { element_type: StorageType::I8, mutable: true })),
547
+ shared: false,
548
+ },
549
+ },
550
+ Slot::Class(name) => {
551
+ let fields = classes.get(name).cloned().unwrap_or_default();
552
+ let field_types: Vec<FieldType> = fields.iter().map(|(_, ty)| FieldType {
553
+ element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
554
+ mutable: true,
555
+ }).collect();
556
+ SubType {
557
+ is_final: true,
558
+ supertype_idx: None,
559
+ composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
560
+ }
561
+ }
562
+ Slot::EnumSuper => SubType {
563
+ is_final: false,
564
+ supertype_idx: None,
565
+ composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: Vec::new().into() }), shared: false },
566
+ },
567
+ Slot::Variant(vname) => {
568
+ let info = enum_variants.get(vname)
569
+ .unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from EnumVariants", vname));
570
+ let super_idx = *registry.enum_super_type_idx.get(&info.enum_name)
571
+ .unwrap_or_else(|| panic!("internal codegen error: enum '{}' missing its supertype slot", info.enum_name));
572
+ let field_types: Vec<FieldType> = info.field_types.iter().map(|ty| FieldType {
573
+ element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
574
+ mutable: false,
575
+ }).collect();
576
+ SubType {
577
+ is_final: true,
578
+ supertype_idx: Some(super_idx),
579
+ composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
580
+ }
581
+ }
582
+ }).collect();
583
+
584
+ module.addGcTypes(subtypes);
585
+
586
+ registry
587
+ }
588
+
380
589
  /// Maps an `ast::ParamType::Fn(params, ret)` to the wasm signature of the *closure
381
590
  /// function* it compiles to: an implicit leading `env_ptr: I32`, then one param per
382
591
  /// declared param type, returning `ret`'s val type (or nothing for `Unit`).
@@ -438,6 +647,12 @@ pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
438
647
  Instruction::I32Const(HEAP_BASE as i32).encode(&mut bump_init);
439
648
  let bump_global = module.addGlobal(ValType::I32, true, &bump_init);
440
649
 
650
+ // Task 1 of the wasm-gc migration: declare the wasm-gc type section entries for
651
+ // every class/enum/Str up front. Purely additive right now — nothing below reads
652
+ // `gc_types` yet, so the bump-allocator codegen this function still emits is
653
+ // completely unaffected; this only proves the new type section is well-formed.
654
+ let gc_types = buildGcTypeRegistry(&mut module, source, &classes, &enum_variants);
655
+
441
656
  let mut func_ids: HashMap<String, u32> = HashMap::new();
442
657
  let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
443
658
  // Closure wasm signature -> function-type index, deduped so every closure/call site
@@ -604,7 +819,7 @@ pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
604
819
  let ctx = CompileCtx {
605
820
  func_ids, func_sigs, classes, methods, enum_variants, enum_params, global_env, bump_global,
606
821
  closures, closure_asts, closure_call_types, named_fn_values,
607
- string_concat_func, int_to_string_func,
822
+ string_concat_func, int_to_string_func, gc_types,
608
823
  };
609
824
 
610
825
  let mut state = ModuleState { next_string_offset: next_static_offset, data_segments: named_fn_data_segments };
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -11,6 +11,19 @@ fn parse(src: &str) -> plum_core::ast::Source {
11
11
  ap.parseSource(tree.root_node())
12
12
  }
13
13
 
14
+ /// The wasm-gc migration (docs/superpowers/plans/2026-07-25-wasm-gc-migration.md)
15
+ /// needs both `wasm_gc` and `wasm_function_references` enabled — confirmed
16
+ /// empirically (see the `wasmtimeGcConfig*` tests below) rather than assumed from
17
+ /// docs, since wasmtime's own doc comment on `Config::wasm_gc` warns its GC support
18
+ /// is still in progress. Every test that instantiates/runs compiled output uses
19
+ /// this shared engine so the whole harness stays on one config.
20
+ fn gcEngine() -> wasmtime::Engine {
21
+ let mut config = wasmtime::Config::new();
22
+ config.wasm_gc(true);
23
+ config.wasm_function_references(true);
24
+ wasmtime::Engine::new(&config).expect("engine with GC config should construct")
25
+ }
26
+
14
27
  #[test]
15
28
  fn compilesToValidWasm() {
16
29
  let src = "fun add(a: Int, b: Int) -> Int =\n a + b\n";
@@ -328,7 +341,7 @@ fun main() -> Int =
328
341
  /// (via wasmparser, above) only proves the module is well-formed — it can't catch
329
342
  /// wrong *values*, so these tests actually execute the compiled output.
330
343
  fn runMain(bytes: &[u8]) -> i64 {
331
- let engine = wasmtime::Engine::default();
344
+ let engine = gcEngine();
332
345
  let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
333
346
  let mut store = wasmtime::Store::new(&engine, ());
334
347
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
@@ -339,7 +352,7 @@ fn runMain(bytes: &[u8]) -> i64 {
339
352
  }
340
353
 
341
354
  fn runMainF64(bytes: &[u8]) -> f64 {
342
- let engine = wasmtime::Engine::default();
355
+ let engine = gcEngine();
343
356
  let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
344
357
  let mut store = wasmtime::Store::new(&engine, ());
345
358
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
@@ -352,7 +365,7 @@ fn runMainF64(bytes: &[u8]) -> f64 {
352
365
  /// Runs a `() -> Str`-returning `main`, reading the length-prefixed
353
366
  /// `[len: u32][utf8 bytes]` string back out of the module's exported memory.
354
367
  fn runMainStr(bytes: &[u8]) -> String {
355
- let engine = wasmtime::Engine::default();
368
+ let engine = gcEngine();
356
369
  let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
357
370
  let mut store = wasmtime::Store::new(&engine, ());
358
371
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
@@ -590,7 +603,7 @@ fun main() -> Int =
590
603
  ";
591
604
  let source = parse(src_trap);
592
605
  let bytes = compileSource(&source).expect("compile failed");
593
- let engine = wasmtime::Engine::default();
606
+ let engine = gcEngine();
594
607
  let module = wasmtime::Module::new(&engine, &bytes).unwrap();
595
608
  let mut store = wasmtime::Store::new(&engine, ());
596
609
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
@@ -611,7 +624,7 @@ fun main() -> Int =
611
624
  ";
612
625
  let source = parse(src);
613
626
  let bytes = compileSource(&source).expect("compile failed");
614
- let engine = wasmtime::Engine::default();
627
+ let engine = gcEngine();
615
628
  let module = wasmtime::Module::new(&engine, &bytes).unwrap();
616
629
  let mut store = wasmtime::Store::new(&engine, ());
617
630
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
@@ -1115,7 +1128,7 @@ fn wasmModuleWithATableElementValidatesAndCallIndirectWorks() {
1115
1128
  let result = wasmparser::validate(&bytes);
1116
1129
  assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
1117
1130
 
1118
- let engine = wasmtime::Engine::default();
1131
+ let engine = gcEngine();
1119
1132
  let wasm_module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
1120
1133
  let mut store = wasmtime::Store::new(&engine, ());
1121
1134
  let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
@@ -1535,3 +1548,470 @@ fun main() -> Int =
1535
1548
  let bytes = compileSource(&source).expect("compile failed");
1536
1549
  assert_eq!(runMain(&bytes), 0);
1537
1550
  }
1551
+
1552
+ #[test]
1553
+ fn wasmtimeGcConfigCanRunAHandEncodedGcModule() {
1554
+ // Throwaway empirical check (plan Task 1, Step 1): confirm wasmtime 28's GC
1555
+ // support actually works end to end before building a type-emitter on top of
1556
+ // it. Hand-encodes the smallest possible module with one GC struct type and
1557
+ // one function that does struct.new_default and returns it, bypassing plum
1558
+ // entirely, so a failure here is unambiguously about wasmtime/wasm-encoder,
1559
+ // not about anything plum-specific.
1560
+ use wasm_encoder::*;
1561
+
1562
+ let mut module = Module::new();
1563
+
1564
+ let mut types = TypeSection::new();
1565
+ // type 0: struct { i32 }
1566
+ types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: true }]);
1567
+ // type 1: () -> (ref null 0)
1568
+ let struct_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
1569
+ types.ty().function(vec![], vec![struct_ref]);
1570
+ module.section(&types);
1571
+
1572
+ let mut funcs = FunctionSection::new();
1573
+ funcs.function(1);
1574
+ module.section(&funcs);
1575
+
1576
+ let mut exports = ExportSection::new();
1577
+ exports.export("main", ExportKind::Func, 0);
1578
+ module.section(&exports);
1579
+
1580
+ let mut code = CodeSection::new();
1581
+ let mut f = Function::new(vec![]);
1582
+ f.instruction(&Instruction::StructNewDefault(0));
1583
+ f.instruction(&Instruction::End);
1584
+ code.function(&f);
1585
+ module.section(&code);
1586
+
1587
+ let bytes = module.finish();
1588
+
1589
+ let mut config = wasmtime::Config::new();
1590
+ config.wasm_gc(true);
1591
+ config.wasm_function_references(true);
1592
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1593
+
1594
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).expect("hand-encoded GC module should be loadable");
1595
+ let mut store = wasmtime::Store::new(&engine, ());
1596
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
1597
+ let main = instance
1598
+ .get_func(&mut store, "main")
1599
+ .expect("main should be exported");
1600
+ let mut results = [wasmtime::Val::I32(0)];
1601
+ main.call(&mut store, &[], &mut results).expect("main should not trap");
1602
+ }
1603
+
1604
+
1605
+ #[test]
1606
+ fn wasmtimeGcConfigSupportsSubtypingRefTestAndNullableSelfReferentialFields() {
1607
+ // Deeper empirical check: enum-variant subtyping (abstract supertype + concrete
1608
+ // subtypes in one `rec` group), ref.test-based dispatch, ref.cast to narrow to a
1609
+ // subtype, and a nullable field that references the struct's OWN type (the
1610
+ // Node.next: Option[Node] shape List needs) — all in one hand-encoded module,
1611
+ // bypassing plum entirely.
1612
+ use wasm_encoder::*;
1613
+
1614
+ let mut module = Module::new();
1615
+ let mut types = TypeSection::new();
1616
+
1617
+ // rec group: type 0 = abstract enum supertype (empty struct, non-final so it can
1618
+ // be subtyped); type 1 = concrete "Some"-like subtype with one i32 payload field;
1619
+ // type 2 = concrete "None"-like subtype (empty, no payload).
1620
+ types.ty().rec(vec![
1621
+ SubType {
1622
+ is_final: false,
1623
+ supertype_idx: None,
1624
+ composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: vec![].into() }), shared: false },
1625
+ },
1626
+ SubType {
1627
+ is_final: true,
1628
+ supertype_idx: Some(0),
1629
+ composite_type: CompositeType {
1630
+ inner: CompositeInnerType::Struct(StructType { fields: vec![
1631
+ FieldType { element_type: StorageType::Val(ValType::I32), mutable: false },
1632
+ ].into() }),
1633
+ shared: false,
1634
+ },
1635
+ },
1636
+ SubType {
1637
+ is_final: true,
1638
+ supertype_idx: Some(0),
1639
+ composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: vec![].into() }), shared: false },
1640
+ },
1641
+ ]);
1642
+
1643
+ // type 3: a self-referential Node struct — { value: i32, next: ref null $Node }.
1644
+ // Must be declared in its own rec group (or alone) referencing its own index (3)
1645
+ // for the nullable self-reference to resolve.
1646
+ types.ty().rec(vec![
1647
+ SubType {
1648
+ is_final: true,
1649
+ supertype_idx: None,
1650
+ composite_type: CompositeType {
1651
+ inner: CompositeInnerType::Struct(StructType { fields: vec![
1652
+ FieldType { element_type: StorageType::Val(ValType::I32), mutable: false },
1653
+ FieldType { element_type: StorageType::Val(ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(3) })), mutable: true },
1654
+ ].into() }),
1655
+ shared: false,
1656
+ },
1657
+ },
1658
+ ]);
1659
+
1660
+ // type 4: () -> i32 — constructs a "Some"-like subtype (type 1) holding 42,
1661
+ // stores it as the supertype (type 0), ref.tests it against type 1, then
1662
+ // ref.casts and struct.gets the payload back out. Also builds a 2-node linked
1663
+ // list (type 3) and confirms unlinking (overwriting `next` with ref.null) and
1664
+ // reading back the remaining node's value both work.
1665
+ let super_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
1666
+ types.ty().function(vec![], vec![ValType::I32]);
1667
+ module.section(&types);
1668
+
1669
+ let mut funcs = FunctionSection::new();
1670
+ funcs.function(4);
1671
+ module.section(&funcs);
1672
+
1673
+ let mut exports = ExportSection::new();
1674
+ exports.export("main", ExportKind::Func, 0);
1675
+ module.section(&exports);
1676
+
1677
+ let mut code = CodeSection::new();
1678
+ let mut f = Function::new(vec![(1, super_ref.clone()), (1, ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(3) }))]);
1679
+ let locals_super = 0u32;
1680
+ let locals_node = 1u32;
1681
+
1682
+ // local_super = Some(42) (as the supertype)
1683
+ f.instruction(&Instruction::I32Const(42));
1684
+ f.instruction(&Instruction::StructNew(1));
1685
+ f.instruction(&Instruction::LocalSet(locals_super));
1686
+
1687
+ // local_node = Node { value: 1, next: null }
1688
+ f.instruction(&Instruction::I32Const(1));
1689
+ f.instruction(&Instruction::RefNull(HeapType::Concrete(3)));
1690
+ f.instruction(&Instruction::StructNew(3));
1691
+ f.instruction(&Instruction::LocalSet(locals_node));
1692
+
1693
+ // if ref.test(local_super, type 1) { result = ref.cast(local_super, type1).field0 } else { result = -1 }
1694
+ f.instruction(&Instruction::LocalGet(locals_super));
1695
+ f.instruction(&Instruction::RefTestNonNull(HeapType::Concrete(1)));
1696
+ f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
1697
+ f.instruction(&Instruction::LocalGet(locals_super));
1698
+ f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(1)));
1699
+ f.instruction(&Instruction::StructGet { struct_type_index: 1, field_index: 0 });
1700
+ f.instruction(&Instruction::Else);
1701
+ f.instruction(&Instruction::I32Const(-1));
1702
+ f.instruction(&Instruction::End);
1703
+
1704
+ // unlink: local_node.next = ref.null (already null, but exercise the store path)
1705
+ f.instruction(&Instruction::LocalGet(locals_node));
1706
+ f.instruction(&Instruction::RefNull(HeapType::Concrete(3)));
1707
+ f.instruction(&Instruction::StructSet { struct_type_index: 3, field_index: 1 });
1708
+
1709
+ // add local_node.value to the ref.test result and return
1710
+ f.instruction(&Instruction::LocalGet(locals_node));
1711
+ f.instruction(&Instruction::StructGet { struct_type_index: 3, field_index: 0 });
1712
+ f.instruction(&Instruction::I32Add);
1713
+ f.instruction(&Instruction::End);
1714
+ code.function(&f);
1715
+ module.section(&code);
1716
+
1717
+ let bytes = module.finish();
1718
+
1719
+ let mut config = wasmtime::Config::new();
1720
+ config.wasm_gc(true);
1721
+ config.wasm_function_references(true);
1722
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1723
+
1724
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
1725
+ let mut store = wasmtime::Store::new(&engine, ());
1726
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
1727
+ let main = instance
1728
+ .get_typed_func::<(), i32>(&mut store, "main")
1729
+ .expect("main should have signature () -> i32");
1730
+ let result = main.call(&mut store, ()).expect("main should not trap");
1731
+ assert_eq!(result, 43, "expected ref.test/ref.cast payload (42) + node.value (1) = 43");
1732
+ }
1733
+
1734
+
1735
+ #[test]
1736
+ fn wasmtimeGcConfigAllowsStructNewInGlobalConstExpr() {
1737
+ // Decision 2 of the wasm-gc migration plan pre-allocates payload-free enum
1738
+ // variants (True/False/None/...) once as globals. Confirm a global's
1739
+ // initializer expression can directly use struct.new (not just i32.const/
1740
+ // ref.null), or the plan needs a `start` function fallback instead.
1741
+ use wasm_encoder::*;
1742
+
1743
+ let mut module = Module::new();
1744
+ let mut types = TypeSection::new();
1745
+ types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
1746
+ types.ty().function(vec![], vec![ValType::I32]);
1747
+ module.section(&types);
1748
+
1749
+ let mut funcs = FunctionSection::new();
1750
+ funcs.function(1);
1751
+ module.section(&funcs);
1752
+
1753
+ let mut globals = GlobalSection::new();
1754
+ let struct_ref_ty = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
1755
+ let mut init = Vec::new();
1756
+ Instruction::I32Const(7).encode(&mut init);
1757
+ Instruction::StructNew(0).encode(&mut init);
1758
+ Instruction::End.encode(&mut init);
1759
+ globals.global(
1760
+ GlobalType { val_type: struct_ref_ty, mutable: false, shared: false },
1761
+ &ConstExpr::raw(init),
1762
+ );
1763
+ module.section(&globals);
1764
+
1765
+ let mut exports = ExportSection::new();
1766
+ exports.export("main", ExportKind::Func, 0);
1767
+ module.section(&exports);
1768
+
1769
+ let mut code = CodeSection::new();
1770
+ let mut f = Function::new(vec![]);
1771
+ f.instruction(&Instruction::GlobalGet(0));
1772
+ f.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
1773
+ f.instruction(&Instruction::End);
1774
+ code.function(&f);
1775
+ module.section(&code);
1776
+
1777
+ let bytes = module.finish();
1778
+
1779
+ let mut config = wasmtime::Config::new();
1780
+ config.wasm_gc(true);
1781
+ config.wasm_function_references(true);
1782
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1783
+
1784
+ let wasm_module = match wasmtime::Module::new(&engine, &bytes) {
1785
+ Ok(m) => m,
1786
+ Err(e) => {
1787
+ println!("struct.new in a global const-expr is NOT supported by this wasmtime/config: {e}");
1788
+ println!("plan implication: Task 2 Step 2 (2a) must use a `start` function instead of a const global initializer.");
1789
+ return;
1790
+ }
1791
+ };
1792
+ let mut store = wasmtime::Store::new(&engine, ());
1793
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
1794
+ let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
1795
+ let result = main.call(&mut store, ()).expect("main should not trap");
1796
+ assert_eq!(result, 7);
1797
+ println!("struct.new IS supported directly in a global const-expr initializer.");
1798
+ }
1799
+
1800
+
1801
+ #[test]
1802
+ fn wasmtimeGcConfigSupportsStartFunctionInitializingGcGlobals() {
1803
+ // Follow-up to the previous test: struct.new isn't allowed in a global
1804
+ // const-expr, so confirm the `start` function fallback works instead —
1805
+ // a mutable global initialized to ref.null, populated by struct.new inside
1806
+ // a `start` function that runs once at instantiation before any export.
1807
+ use wasm_encoder::*;
1808
+
1809
+ let mut module = Module::new();
1810
+ let mut types = TypeSection::new();
1811
+ types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
1812
+ types.ty().function(vec![], vec![]); // start fn: () -> ()
1813
+ types.ty().function(vec![], vec![ValType::I32]); // main: () -> i32
1814
+ module.section(&types);
1815
+
1816
+ let mut funcs = FunctionSection::new();
1817
+ funcs.function(1); // func 0: start
1818
+ funcs.function(2); // func 1: main
1819
+ module.section(&funcs);
1820
+
1821
+ let mut globals = GlobalSection::new();
1822
+ let struct_ref_ty = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
1823
+ globals.global(
1824
+ GlobalType { val_type: struct_ref_ty.clone(), mutable: true, shared: false },
1825
+ &ConstExpr::ref_null(HeapType::Concrete(0)),
1826
+ );
1827
+ module.section(&globals);
1828
+
1829
+ let mut exports = ExportSection::new();
1830
+ exports.export("main", ExportKind::Func, 1);
1831
+ module.section(&exports);
1832
+
1833
+ let start = StartSection { function_index: 0 };
1834
+ module.section(&start);
1835
+
1836
+ let mut code = CodeSection::new();
1837
+ let mut start_fn = Function::new(vec![]);
1838
+ start_fn.instruction(&Instruction::I32Const(99));
1839
+ start_fn.instruction(&Instruction::StructNew(0));
1840
+ start_fn.instruction(&Instruction::GlobalSet(0));
1841
+ start_fn.instruction(&Instruction::End);
1842
+ code.function(&start_fn);
1843
+
1844
+ let mut main_fn = Function::new(vec![]);
1845
+ main_fn.instruction(&Instruction::GlobalGet(0));
1846
+ main_fn.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
1847
+ main_fn.instruction(&Instruction::End);
1848
+ code.function(&main_fn);
1849
+ module.section(&code);
1850
+
1851
+ let bytes = module.finish();
1852
+
1853
+ let mut config = wasmtime::Config::new();
1854
+ config.wasm_gc(true);
1855
+ config.wasm_function_references(true);
1856
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1857
+
1858
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
1859
+ let mut store = wasmtime::Store::new(&engine, ());
1860
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate (start fn should run automatically)");
1861
+ let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
1862
+ let result = main.call(&mut store, ()).expect("main should not trap");
1863
+ assert_eq!(result, 99, "start fn should have populated the global before main ran");
1864
+ }
1865
+
1866
+
1867
+ #[test]
1868
+ fn wasmtimeGcConfigSupportsArrayNewDataFromPassiveSegmentWithNoMemorySection() {
1869
+ // Decision 4 of the wasm-gc migration plan: static string data lives in a
1870
+ // PASSIVE data segment (no active memory offset), consumed via array.new_data
1871
+ // — confirming this needs no `memory` section in the module at all, which is
1872
+ // what lets the whole memory section disappear once bump allocation is retired.
1873
+ use wasm_encoder::*;
1874
+
1875
+ let mut module = Module::new();
1876
+ let mut types = TypeSection::new();
1877
+ types.ty().array(&StorageType::I8, false);
1878
+ let arr_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
1879
+ types.ty().function(vec![], vec![ValType::I32]);
1880
+ module.section(&types);
1881
+
1882
+ let mut funcs = FunctionSection::new();
1883
+ funcs.function(1);
1884
+ module.section(&funcs);
1885
+
1886
+ let mut exports = ExportSection::new();
1887
+ exports.export("main", ExportKind::Func, 0);
1888
+ module.section(&exports);
1889
+
1890
+ // Required whenever the module uses array.new_data/memory.init/data.drop —
1891
+ // the validator needs the passive-segment count before the code section.
1892
+ module.section(&DataCountSection { count: 1 });
1893
+
1894
+ let mut code = CodeSection::new();
1895
+ let mut f = Function::new(vec![(1, arr_ref)]);
1896
+ // local 0 = array.new_data(type 0, data segment 0) with offset=0, len=5 ("hello")
1897
+ f.instruction(&Instruction::I32Const(0)); // data offset
1898
+ f.instruction(&Instruction::I32Const(5)); // length
1899
+ f.instruction(&Instruction::ArrayNewData { array_type_index: 0, array_data_index: 0 });
1900
+ f.instruction(&Instruction::LocalSet(0));
1901
+ // return array.get(local0, 0) — the byte 'h' = 104
1902
+ f.instruction(&Instruction::LocalGet(0));
1903
+ f.instruction(&Instruction::I32Const(0));
1904
+ f.instruction(&Instruction::ArrayGetU(0));
1905
+ f.instruction(&Instruction::End);
1906
+ code.function(&f);
1907
+ module.section(&code);
1908
+
1909
+ // NOTE: deliberately no MemorySection at all.
1910
+ let mut data = DataSection::new();
1911
+ data.passive(b"hello".iter().copied());
1912
+ module.section(&data);
1913
+
1914
+ let bytes = module.finish();
1915
+
1916
+ let mut config = wasmtime::Config::new();
1917
+ config.wasm_gc(true);
1918
+ config.wasm_function_references(true);
1919
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1920
+
1921
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module with no memory section + passive data should be loadable: {e}"));
1922
+ let mut store = wasmtime::Store::new(&engine, ());
1923
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
1924
+ let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
1925
+ let result = main.call(&mut store, ()).expect("main should not trap");
1926
+ assert_eq!(result, b'h' as i32);
1927
+ }
1928
+
1929
+
1930
+ #[test]
1931
+ fn wasmtimeGcConfigSupportsWideningConcreteStructRefToAnyrefAndCastingBack() {
1932
+ // Decision 3 of the wasm-gc migration plan: closure env pointers are `anyref`
1933
+ // in the shared call_indirect signature, with each closure's body ref.cast-ing
1934
+ // back to its own concrete env struct type. Confirm a concrete struct ref can
1935
+ // be stored where anyref is expected (implicit widening, no instruction needed)
1936
+ // and RefCastNonNull(Concrete(_)) recovers the concrete type correctly.
1937
+ use wasm_encoder::*;
1938
+
1939
+ let mut module = Module::new();
1940
+ let mut types = TypeSection::new();
1941
+ types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
1942
+ // "identity-ish" function: (anyref) -> i32, casts back to concrete type 0 and reads field 0.
1943
+ types.ty().function(vec![ValType::Ref(RefType::ANYREF)], vec![ValType::I32]);
1944
+ // main: () -> i32, builds a concrete struct, passes it (widened) to func 1.
1945
+ types.ty().function(vec![], vec![ValType::I32]);
1946
+ module.section(&types);
1947
+
1948
+ let mut funcs = FunctionSection::new();
1949
+ funcs.function(1); // func 0: the anyref-accepting fn
1950
+ funcs.function(2); // func 1: main
1951
+ module.section(&funcs);
1952
+
1953
+ let mut exports = ExportSection::new();
1954
+ exports.export("main", ExportKind::Func, 1);
1955
+ module.section(&exports);
1956
+
1957
+ let mut code = CodeSection::new();
1958
+ let mut cast_fn = Function::new(vec![]);
1959
+ cast_fn.instruction(&Instruction::LocalGet(0));
1960
+ cast_fn.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(0)));
1961
+ cast_fn.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
1962
+ cast_fn.instruction(&Instruction::End);
1963
+ code.function(&cast_fn);
1964
+
1965
+ let mut main_fn = Function::new(vec![]);
1966
+ main_fn.instruction(&Instruction::I32Const(55));
1967
+ main_fn.instruction(&Instruction::StructNew(0)); // pushes (ref 0) — implicitly a subtype of anyref
1968
+ main_fn.instruction(&Instruction::Call(0)); // call expects anyref param — implicit widening at the call site
1969
+ main_fn.instruction(&Instruction::End);
1970
+ code.function(&main_fn);
1971
+ module.section(&code);
1972
+
1973
+ let bytes = module.finish();
1974
+
1975
+ let mut config = wasmtime::Config::new();
1976
+ config.wasm_gc(true);
1977
+ config.wasm_function_references(true);
1978
+ let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
1979
+
1980
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
1981
+ let mut store = wasmtime::Store::new(&engine, ());
1982
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
1983
+ let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
1984
+ let result = main.call(&mut store, ()).expect("main should not trap");
1985
+ assert_eq!(result, 55, "concrete struct ref should widen to anyref implicitly and cast back correctly");
1986
+ }
1987
+
1988
+ #[test]
1989
+ fn gcTypeRegistryProducesAWellFormedTypeSectionAlongsideBumpAllocatorCodegen() {
1990
+ // Task 1 Step 5 of the wasm-gc migration plan: the new (currently unconsumed)
1991
+ // wasm-gc type registry declares a well-formed type section — a struct type per
1992
+ // class, a supertype+subtypes set per enum (including the built-in Bool), and a
1993
+ // shared Str array type — even though every OTHER part of this compiled module
1994
+ // still uses the old bump-allocator representation. Exercises a class, an enum
1995
+ // with both a payload and a payload-free variant, and Str, so all three GC type
1996
+ // shapes actually get emitted.
1997
+ let src = "\
1998
+ type Cat =
1999
+ name: Str
2000
+ age: Int
2001
+
2002
+ enum Option =
2003
+ | Some[Int]
2004
+ | None
2005
+
2006
+ fun main() -> Int =
2007
+ c = Cat(name: \"x\", age: 7)
2008
+ c.age
2009
+ ";
2010
+ let source = parse(src);
2011
+ let bytes = compileSource(&source).expect("compile failed");
2012
+ let result = wasmparser::validate(&bytes);
2013
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
2014
+ // And the still-untouched bump-allocator codegen must still actually run correctly —
2015
+ // this task is purely additive, nothing behavioral should have changed.
2016
+ assert_eq!(runMain(&bytes), 7);
2017
+ }
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -17,6 +17,15 @@ fn parseFile(name: &str) -> plum_core::ast::Source {
17
17
  ap.parseSource(tree.root_node())
18
18
  }
19
19
 
20
+ /// See the identically-named helper in codegen_tests.rs for why both features
21
+ /// are needed (part of the wasm-gc migration, docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
22
+ fn gcEngine() -> wasmtime::Engine {
23
+ let mut config = wasmtime::Config::new();
24
+ config.wasm_gc(true);
25
+ config.wasm_function_references(true);
26
+ wasmtime::Engine::new(&config).expect("engine with GC config should construct")
27
+ }
28
+
20
29
  fn assertCompiles(name: &str) -> Vec<u8> {
21
30
  let source = parseFile(name);
22
31
  let bytes = compileSource(&source).unwrap_or_else(|e| panic!("{} failed to compile: {}", name, e));
@@ -54,7 +63,7 @@ fn typesCompiles() {
54
63
  fn methodsCompilesAndRunsCorrectly() {
55
64
  let bytes = assertCompiles("methods.plum");
56
65
 
57
- let engine = wasmtime::Engine::default();
66
+ let engine = gcEngine();
58
67
  let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
59
68
  let mut store = wasmtime::Store::new(&engine, ());
60
69
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
@@ -72,7 +81,7 @@ fn methodsCompilesAndRunsCorrectly() {
72
81
  #[test]
73
82
  fn matchExampleCompilesAndRunsCorrectly() {
74
83
  let bytes = assertCompiles("match.plum");
75
- let engine = wasmtime::Engine::default();
84
+ let engine = gcEngine();
76
85
  let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
77
86
  let mut store = wasmtime::Store::new(&engine, ());
78
87
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
@@ -89,7 +98,7 @@ fn matchExampleCompilesAndRunsCorrectly() {
89
98
  #[test]
90
99
  fn closuresExampleCompilesAndRunsCorrectly() {
91
100
  let bytes = assertCompiles("closures.plum");
92
- let engine = wasmtime::Engine::default();
101
+ let engine = gcEngine();
93
102
  let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
94
103
  let mut store = wasmtime::Store::new(&engine, ());
95
104
  let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");