plum

#treesitter#compiler#wasm

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

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


32da118Peter John 2026-07-20T19:07:02+05:30
docs: add design spec for generic-enum multi-instantiation support
docs/superpowers/specs/2026-07-20-generic-enum-multi-instantiation-design.md ADDED
@@ -0,0 +1,74 @@
1
+ # Generic enums: support more than one concrete instantiation per program
2
+
3
+ ## Problem
4
+
5
+ Generic enum monomorphization (from the prior generics-monomorphization work) has a real,
6
+ documented limitation: a given generic enum may only be instantiated at **one** concrete type per
7
+ program. Instantiating the same generic enum at two different concrete types (e.g. both
8
+ `Option<Int>` and `Option<Str>` needed in the same file) is detected and rejected with a clear
9
+ `monomorphize:` error rather than silently corrupting anything — but it's still a real limitation
10
+ for exactly the kind of general-purpose `Option`/`Result` a real program would want.
11
+
12
+ Root cause: `plum_checker::build_global_tables` builds `EnumVariants` — the table matching a
13
+ `match` pattern's or a construction call's bare variant name (`"Some"`, `"None"`) to its owning
14
+ enum, tag, and field types — keyed by **bare variant name, globally across the whole program**.
15
+ Two specializations of the same generic enum (`Option$Int`, `Option$Str`) both declare a variant
16
+ literally named `Some`, so they'd collide in that flat table. The existing monomorphizer detects
17
+ this collision and rejects it rather than letting one specialization silently clobber the other's
18
+ registration — correct, but overly conservative.
19
+
20
+ ## Fix
21
+
22
+ Mangle variant names too, using the same suffix as their enum's own mangled name — `Some` →
23
+ `Some$Int` / `Some$Str`, `None` → `None$Int` / `None$Str`. This is necessary even for
24
+ payload-free variants like `None`: although their runtime representation (a small tag, no
25
+ payload) is identical regardless of the concrete type argument, their *static type* differs per
26
+ instantiation (a bare `None` used as a value must type as `TNamed("Option$Int")` or
27
+ `TNamed("Option$Str")`, not an ambiguous, instantiation-independent `Option`), so leaving them
28
+ unmangled would still create real inference ambiguity even though it wouldn't corrupt anything
29
+ at the runtime-representation level.
30
+
31
+ This touches three points, all inside the already-built `plum_checker::monomorphize` pass — no
32
+ new pipeline integration is needed, since `monomorphize_source` already runs ahead of both
33
+ `check_source` and `compile_source`:
34
+
35
+ 1. **`specialize_enum`**: extend it to rename each variant using the same mangled suffix as the
36
+ enum's own name, not just the enum itself.
37
+ 2. **Construction call-site rewriting** (`resolve_enum_instantiation`): currently deliberately
38
+ does *not* rewrite `call.name` for a variant constructor like `Some(5)`, on the theory that
39
+ variant names stay bare. That has to change now: once the enum's own instantiation is
40
+ resolved, also rewrite the construction call's name from `"Some"` to its mangled form.
41
+ 3. **Match-pattern rewriting** (new): `rewrite_stmt`'s existing `Match` handling infers the
42
+ subject's type (already needed for binding a non-variant pattern name) but never touches the
43
+ patterns themselves. It needs to: check whether the inferred subject type is a specialized
44
+ generic enum, and if so, rewrite every `Some`/`None`-shaped pattern (`ast::CasePattern::Name`
45
+ for a payload-free tag, `ast::CasePattern::Class` for a constructor pattern) in that match's
46
+ cases to the corresponding mangled variant name, using a per-enum variant-mangling table
47
+ recorded at the moment that enum specialization was produced.
48
+
49
+ With variant names uniquely mangled per specialization, the existing collision-detection
50
+ machinery (the `enum_variant_owner` ownership table, and the error path it guards) becomes
51
+ unnecessary and should be removed — there is no more collision to detect, since every
52
+ specialization's variants live under their own unique mangled names.
53
+
54
+ ## Testing plan
55
+
56
+ - **Checker tests**: a generic `Option`-shaped enum instantiated at two different concrete types
57
+ in the same program (both `Option<Int>` and `Option<Str>` constructed and matched) type-checks
58
+ correctly, each `match`'s patterns resolving against the correct specialization.
59
+ - **Codegen tests**: the same two-instantiation scenario, compiled and executed via `wasmtime`,
60
+ confirming both specializations produce correct, non-aliasing results (mirroring the existing
61
+ "generic class specialized at two types" test's shape, now extended to enums).
62
+ - Re-verify (don't just assume unaffected): a single-instantiation generic enum still works
63
+ exactly as before now that the collision-guard code path is gone — the existing
64
+ single-instantiation tests from the prior plan must still pass unchanged.
65
+ - Remove or repurpose the now-obsolete "multi-instantiation collision is a clear error" test from
66
+ the prior plan, since that behavior is being replaced by "multi-instantiation now works
67
+ correctly" — replace it with the new coexistence test above.
68
+
69
+ ## Out of scope
70
+
71
+ - Everything else the generics-monomorphization plan already scoped out (a method introducing
72
+ its own additional generic parameter; trait-bound enforcement; `libs/std` compiling as-is) —
73
+ unchanged, unaffected by this fix.
74
+ - Closures and the `list.plum`/`map.plum` rewrite — separate, subsequent follow-ups.