plum

#treesitter#compiler#wasm

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

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


docs/superpowers/specs/2026-07-25-wasm-gc-migration-design.md
# Design: Migrate plum-wasm-codegen to wasm-gc

## Motivation

`plum-wasm-codegen` currently hand-rolls its own memory model directly on top of `wasm_encoder`: a single linear memory, a `bump_global` pointer, and manually computed byte offsets for every struct, enum, and string (`plum-wasm-codegen/src/lib.rs:73,331,433`). This allocator only ever grows — it never resizes and never frees — which is why `libs/std`'s `List` and `Map` don't fully compile today: `List` is a doubly-linked list of `Node[T]` structs (`head`/`tail`/`prev`/`next: Option[Node]`), and mutating methods (`add`, `set`, `removeAt`, `remove`, `clear`, `reverse`) are all still `todo` in `libs/std/list.plum`. Splicing nodes in and out of a linked list needs a memory model that can represent nullable/self-referential struct fields and reclaim unlinked nodes — the bump allocator can do neither.

wasmtime (already a dev-dependency at version 28) and `wasm_encoder` (0.220) both support the wasm-gc proposal (`struct`/`array` types, `ref`/`ref null` types, `struct.new`/`struct.get`, `ref.test`/`br_on_cast`). Migrating plum's struct/class/enum/string representation onto wasm-gc directly solves the growable/freeable memory gap: unlinking a `Node` just drops the last reference to it, and the engine's GC reclaims it automatically, with no manual free bookkeeping and no risk of the current unbounded bump-allocator growth.

## Scope

In scope:
- Migrate structs and classes to one wasm-gc `struct` type per concrete (post-monomorphization) type.
- Migrate enums to per-variant subtyping: one abstract supertype `struct` plus one concrete subtype `struct` per variant, matched via `ref.test`/`br_on_cast`. `Option[T]` is generated the same way as any other enum (`Some`/`None` as ordinary subtypes) — no null-ref special-casing, for full uniformity with the rest of the type system.
- Migrate `Str` to a wasm-gc `array<i8>`, rewriting the string-concat and int-to-string helpers (`plum-wasm-codegen/src/lib.rs:648,757`) against `array.new`/`array.copy` instead of bump-memory writes.
- Retire the bump allocator, `MemoryType`, and `bump_global` entirely once nothing depends on them.
- Implement `libs/std/list.plum`'s currently-`todo` methods (`add`, `set`, `removeAt`, `remove`, `clear`, `reverse`) against the new struct representation — this is the concrete deliverable the migration unblocks. `Map` (built on `List`) follows automatically.
- Update the `wasmtime` test harness (`codegen_tests.rs`, `examples_test.rs`) to an `Engine`/`Config` that has the GC proposal enabled, and extend existing tests to cover the new encoding plus the newly-working `List`/`Map` methods.

Out of scope (explicitly deferred to future projects):
- A `[T]` array literal/indexing language feature. Not needed for this project — `List`'s underlying representation is a linked list, not a contiguous array, and wasm-gc `array` types are fixed-size at creation (no native resize primitive), so they wouldn't simplify `List`'s growth story anyway. A first-class array type may be worth adding later for genuinely contiguous/indexed use cases, as its own spec.
- wasm exception-handling (`try_table`/`throw`) for error propagation. Plum's existing `Option`/`Result` enum-based error handling doesn't need this to keep working; exceptions get their own spec once GC types exist to carry payloads through throw/catch, if ever pursued.
- Any change to `plum-checker`'s type-checking or monomorphization logic — monomorphization already resolves every generic instantiation to a concrete type before codegen runs, so this migration needs no generic wasm-gc types; it emits one concrete GC type per concrete instantiation, the same granularity bump-layout codegen uses today.

## Architecture

The type section of the emitted module declares:
- One wasm-gc `struct` type per concrete struct/class, with fields typed as `ref`/`ref null` (for struct/enum-typed fields), `i32` (`Int`/`Bool`), or `f64` (`Float`).
- One `array<i8>` type for `Str`.
- Per enum, one abstract supertype `struct` and one concrete subtype `struct` per variant, declared with wasm-gc's subtyping relation so a variant value can be used wherever the enum's supertype is expected.

Construction becomes `struct.new`/`struct.new_default` in place of bump-pointer arithmetic. Field access becomes `struct.get`/`struct.set`. Recursive/nullable fields — e.g. `List`'s `Node.next: Option[Node]` — become plain `ref`/`ref null` fields: unlinking a node is just overwriting a field with `ref.null`, after which the old node is unreachable and reclaimed by the engine's GC with no explicit free.

## Components

- **Type-section emitter** (new): walks the checker's monomorphized struct/class/enum declarations and emits the wasm-gc composite types described above, replacing today's manual offset/layout computation.
- **Construction codegen** (`ast::Expr::New` and enum-variant construction, currently around `plum-wasm-codegen/src/lib.rs:2888-2963`): rewritten to `struct.new`/`struct.new_default` against the declared type index, instead of `GlobalGet`/`GlobalSet` bump-pointer bookkeeping.
- **Field/variant access codegen**: struct field reads/writes become `struct.get`/`struct.set` on the known concrete type index. Enum payload access adds a `ref.cast` to the variant's subtype first, since the static type at a match arm is the narrowed subtype.
- **Match dispatch codegen** (currently integer tag comparisons): rewritten to `ref.test`/`br_on_cast` against each variant's subtype in declaration order, falling through to a trap for exhaustiveness — the same shape as today's "no matching tag" fallthrough.
- **String helpers** (`register_string_concat_helper`, `register_int_to_string_helper`): rewritten against `array.new`/`array.copy` instead of bump-memory writes.
- **`libs/std/list.plum` / `map.plum`**: no representation-driven source changes needed; `add`, `set`, `removeAt`, `remove`, `clear`, `reverse` get real implementations now that `struct.new`/null-ref splicing makes them straightforward. This is the concrete unblock this project delivers.
- **Test harness** (`codegen_tests.rs`, `examples_test.rs`): `wasmtime::Engine::default()` calls updated to an explicit `Config` with the GC proposal enabled — first confirming whether wasmtime 28's default already enables it, since every other test in the migration depends on this.

## Data Flow

The overall pipeline (parse → check/monomorphize → codegen → wasm bytes) is unchanged. Within codegen, the walk over monomorphized types now emits wasm-gc type declarations up front and records each type's assigned GC type index, instead of computing byte offsets into a shared linear memory. Every construction, field access, and match site looks up the relevant type index through the existing `func_ids`/`classes`/`enum_variants` tables (`plum-wasm-codegen/src/lib.rs:251-259`), extended to also carry that index. A generic instantiation like `List[Int]` vs `List[Str]` still gets its own fully concrete type — now as a distinct GC struct type index rather than a distinct offset layout.

## Error Handling

Type errors continue to be fully caught by `plum-checker` before codegen runs; codegen continues to assume a well-typed, monomorphized AST. Codegen-side "not yet supported" gaps (in the style of today's `Float`-interpolation error) continue to report clear errors during the migration rather than silently falling back to bump-allocator codegen for anything not yet converted. At runtime, `match` exhaustiveness failures keep trapping exactly as today, now via a fallthrough after the last `br_on_cast` instead of after the last tag comparison — no new panic/exception mechanism is introduced, consistent with exceptions being out of scope.

## Testing

`codegen_tests.rs` and `examples_test.rs` already round-trip generated wasm through `wasmtime` and assert on execution results; these are extended rather than replaced:
- Every existing struct/enum/string test is re-verified against the new GC encoding.
- New tests cover the concrete unblock: `List.add`/`set`/`removeAt`/`remove`/`clear`/`reverse`, and `Map` built on top of `List`.
- New tests cover recursive nullable fields specifically (`Node.next: Option[Node]`), confirming an unlinked node becomes unreachable and is reclaimed rather than leaked.
- The `wasmtime::Engine`/`Config` used in tests is confirmed to support the GC proposal first, since it gates every other test in the migration.