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-20-generics-monomorphization-design.md
ea186e4 1
# Generics monomorphization
ea186e4 2
ea186e4 3
## Problem
ea186e4 4
ea186e4 5
Generic type parameters (single lowercase letters: `a`, `b`, `c`, `d`) are supported today only
ea186e4 6
at the syntax and permissive-type-check level: `type Box(a) = value: a`, `trait Comparable(a: Ord)
ea186e4 7
= compareTo(other: a) -> Int`, and generic function params (`wrap(value: a) -> Bool`, `pair(first:
ea186e4 8
a, second: b) -> Bool`) all parse and type-check (`plum-checker` treats an unresolved generic
ea186e4 9
param permissively, the same way it treats any unmodeled/unknown type). `plum-wasm-codegen` has no
ea186e4 10
representation for a generic parameter at all — any attempt to compile a generic class/function
ea186e4 11
hits its permissive-fallback path (`ValType::I32` treated as "pointer to unmodeled type") without
ea186e4 12
ever resolving what concrete type is actually present at a given call site, so field
ea186e4 13
loads/stores and arithmetic on generic-typed values compile with the wrong width or fail outright.
ea186e4 14
ea186e4 15
This is the single largest remaining gap blocking `libs/std`'s actual `Option`/`Result`/`List`/
ea186e4 16
`Map` from compiling — **but fixing generics alone does not get there**: `libs/std/list.plum` and
ea186e4 17
`map.plum` also use syntax that doesn't exist in the grammar at all today (closures `|v| ...`,
ea186e4 18
`Nil`, `?.` optional chaining, `:=`, decorators like `@HasTrait`, colon-arrow return types). This
ea186e4 19
work targets **only** the currently-documented generic syntax (the shapes already exercised in
ea186e4 20
`examples/types.plum` and `examples/functions.plum`), not those std lib files.
ea186e4 21
ea186e4 22
## Approach: monomorphization by specialization
ea186e4 23
ea186e4 24
Chosen over a type-erasure/boxed-uniform-representation approach because the rest of the compiler
ea186e4 25
already assumes every value has a statically-known concrete wasm type (`I32`/`I64`/`F64`) at
ea186e4 26
codegen time — monomorphization preserves that invariant everywhere, at the cost of compiling one
ea186e4 27
specialized copy per concrete instantiation actually used (standard Rust/C++-template-style
ea186e4 28
trade-off), rather than requiring a new runtime representation (boxed/tagged values, out-of-band
ea186e4 29
type metadata) that the rest of the codegen doesn't have anywhere else.
ea186e4 30
ea186e4 31
### 1. Instantiation-site collection
ea186e4 32
ea186e4 33
Walk the whole program to determine every concrete `(generic item, concrete type argument list)`
ea186e4 34
combination actually needed:
ea186e4 35
ea186e4 36
- Explicit concrete type annotations naming a generic item with concrete arguments (`Box[Int]`,
ea186e4 37
  `Box(Int)` — both bracket and paren syntax are already accepted per the grammar).
ea186e4 38
- Constructor calls where a generic class's field types resolve the class's generic parameters via
ea186e4 39
  unification against the supplied argument types (`Box(value: 5)` → `a = Int`).
ea186e4 40
- Generic function/method calls where argument types resolve the function's generic parameters via
ea186e4 41
  unification (`wrap(5)` → `a = Int`; `pair(1, "x")` → `a = Int, b = Str`).
ea186e4 42
ea186e4 43
This reuses `plum-checker`'s existing unification (`unify`, `infer_expr`) — extended so a generic
ea186e4 44
parameter name occurring in a declared type position is treated as a fresh unification variable
ea186e4 45
bound per call site, instead of being ignored/treated as an opaque unmodeled type name.
ea186e4 46
ea186e4 47
### 2. Specialization mechanism
ea186e4 48
ea186e4 49
For each unique `(generic item, concrete type args)` pair, produce a substituted copy of the
ea186e4 50
`Class`/`Fn` AST item: every occurrence of a generic parameter name in a **declared type
ea186e4 51
position** (field types, param types, return types) is replaced by its concrete resolved
ea186e4 52
`ast::Type`, under an internal mangled name (e.g. `Box$Int`, `wrap$Int`, `pair$Int$Str` — `$`
ea186e4 53
joins the base name and each concrete type argument in declaration order). The substituted copy is
ea186e4 54
then run through the **existing, unmodified** non-generic checker (`build_global_tables`,
ea186e4 55
`check_fn`) and codegen (`compile_fn_body`, class field layout) pipeline — neither of those needs
ea186e4 56
to know or care that its input started out generic.
ea186e4 57
ea186e4 58
Mangled names are purely internal (`ClassEnv`/`MethodEnv`/`func_ids` keys, wasm export names where
ea186e4 59
relevant) — user source keeps calling `Box(...)`/`wrap(...)` normally; the compiler resolves which
ea186e4 60
specialization a given call site needs from its own already-inferred concrete argument types and
ea186e4 61
rewrites the call/construction internally to reference the matching mangled specialization.
ea186e4 62
a10644c 63
**Generic enums** (`enum Option = | Some(a) | None` — matching `libs/std`'s real `Option`) are in
a10644c 64
scope alongside classes/functions/traits. Unlike `Class`/`Trait`, `ast::Enum` has no explicit
a10644c 65
`generics: Vec<GenericParam>` field, and neither does `ast::Fn` — both are detected as generic
a10644c 66
implicitly, the same way: a variant's field type name (`EnumVariant.fields: Vec<String>`, for
a10644c 67
enums) or a function's param/return type name (for `Fn`) that is a single lowercase letter (`a`,
a10644c 68
`b`, `c`, `d` — the grammar's only legal generic-parameter spelling) is treated as an implicit
a10644c 69
generic parameter, exactly like a `Class`'s explicit `generics` list entry. Specialization,
a10644c 70
mangling, and the worklist apply identically regardless of which item kind introduced the generic
a10644c 71
parameter.
a10644c 72
ea186e4 73
A generic **method** (`get<List>(i: Int) -> a`) specializes alongside its receiver class: once
ea186e4 74
`List` is specialized as `List$Int`, every method declared with receiver `List` gets a
ea186e4 75
correspondingly substituted, separately-specialized copy keyed `List$Int::get`, following the
ea186e4 76
existing `MethodEnv` key shape (`(receiver type name, method name)`) with the receiver name being
ea186e4 77
the mangled one.
ea186e4 78
ea186e4 79
### 3. Transitive/recursive generics — worklist to a fixed point
ea186e4 80
ea186e4 81
A freshly-specialized body can itself call another still-generic function/method — now with
ea186e4 82
concrete types known throughout that specialized body, so this is a new, fully-resolved
ea186e4 83
instantiation site. Processed as a worklist: seed it from every concrete instantiation site
ea186e4 84
reachable from non-generic code, specialize each, scan the newly-produced specialized bodies for
ea186e4 85
further instantiation sites they introduce, and repeat until no new specializations are added.
ea186e4 86
ea186e4 87
A recursion-depth guard bounds this (e.g. a hard cap on total specializations per compile, or on
ea186e4 88
nesting depth of a type argument list) so a pathological unbounded-expanding generic (a generic
ea186e4 89
type that embeds a larger instantiation of itself, with no concrete base case) fails with a clear
ea186e4 90
compile error rather than hanging or exhausting memory.
ea186e4 91
ea186e4 92
## Out of scope
ea186e4 93
ea186e4 94
- Trait-bound enforcement (`trait Comparable(a: Ord)`'s `Ord` bound) — stays exactly as permissive
ea186e4 95
  as it is today. Monomorphization only needs to substitute concrete types through, not verify
ea186e4 96
  bounds are satisfied.
ea186e4 97
- Generic code with zero reachable concrete instantiation anywhere in the program is simply never
ea186e4 98
  specialized or compiled — this tree-shaking falls out of the worklist approach for free, it
ea186e4 99
  isn't a separate feature.
ea186e4 100
- `libs/std/list.plum` and `map.plum` compiling as-is — as established above, they need several
ea186e4 101
  other unimplemented language features first (closures, `Nil`/optional chaining, decorators,
ea186e4 102
  colon-arrow return syntax) that are unrelated to generics and out of scope here.
ea186e4 103
- A general, unbounded-generic-recursion *detector* beyond a simple depth/count cap — this is a
ea186e4 104
  defensive backstop against a degenerate program, not a soundness analysis.
ea186e4 105
ea186e4 106
## Testing plan
ea186e4 107
ea186e4 108
- **Checker tests** (`plum-checker/tests/checker_tests.rs`): a generic class instantiated at two
ea186e4 109
  different concrete types in one program (`Box(Int)` and `Box(Str)`) resolves each usage's field
ea186e4 110
  access to the correct concrete type; a generic function called with different concrete argument
ea186e4 111
  types at different call sites infers correctly per site; a multi-param generic (`Pair`/`Map`-
ea186e4 112
  shaped, two independent type params) resolves both params independently per call site.
ea186e4 113
- **Codegen tests** (`plum-wasm-codegen/tests/codegen_tests.rs`, compiled and executed via
ea186e4 114
  `wasmtime` matching existing style): a generic class specialized at `Int` and at `Str` in the
ea186e4 115
  same program, confirming the two specializations don't alias (distinct compiled bodies, correct,
ea186e4 116
  independent field widths/offsets); a generic function called at multiple concrete types in one
ea186e4 117
  program; a generic method dispatched on a specialized generic class; a transitively-generic call
ea186e4 118
  chain (a specialized generic function itself calling another still-generic function) proving the
ea186e4 119
  worklist reaches a correct fixed point; a deliberately-pathological unbounded-recursive generic
ea186e4 120
  type, asserting `compile_source` returns a clear error rather than hanging or panicking.
ea186e4 121
- **Examples**: extend `examples/types.plum`/`examples/functions.plum` (currently declarations
ea186e4 122
  only) with actual instantiations and a `main` that runs them end-to-end. Update README's Known
ea186e4 123
  Gaps to drop the "user-defined generics aren't monomorphized" bullet, while adding/keeping a note
ea186e4 124
  that this alone doesn't unblock `libs/std`'s actual `List`/`Map`/`Option`/`Result` (separate,
ea186e4 125
  unrelated gaps remain there).