plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
32da118
— Peter 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.
|