plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
aedebc1
— Peter John
2026-07-20T19:28:05+05:30
docs: add implementation plan for generic-enum multi-instantiation
docs/superpowers/plans/2026-07-20-generic-enum-multi-instantiation.md
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# Generic Enum Multi-Instantiation Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Remove the "a generic enum may only be instantiated at one concrete type per program" limitation, so `Option<Int>` and `Option<Str>` (or any two concrete instantiations of the same generic enum) can coexist in one program.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Mangle variant names with the same suffix as their enum's own mangled name (`Some` → `Some$Int`/`Some$Str`, `None` → `None$Int`/`None$Str`), rewrite variant-construction call sites to reference the mangled variant name, and rewrite `match` pattern names against the correct specialization (resolved from the subject's inferred concrete type). All changes are contained inside the already-built `plum_checker::monomorphize` module — no new pipeline integration is needed. The existing collision-detection guard (which rejected a second instantiation rather than corrupting the first) becomes unnecessary and is removed, since variant names are now uniquely mangled per specialization.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Rust (`plum-checker` crate only for this plan — `plum-core`/`plum-wasm-codegen` need no changes).
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- **Narrower residual limitation, replacing the old one:** a payload-free variant (e.g. `None`) used as a *bare value outside of a `match` pattern* (i.e. parsed as `ast::Expr::TypeName`, not inside a case pattern) still can't be disambiguated between multiple concrete instantiations of its enum, since nothing at that expression alone pins down which instantiation it belongs to. This is out of scope to fix here — such usage will fail to resolve cleanly (an "unknown"/unmodeled-name error from the checker or codegen) rather than silently misbehaving, which is an acceptable, documented trade-off. Constructing via a payload-carrying sibling (`Some(5)`) and matching (`Some(v) => ...`, `None => ...`) — the overwhelmingly common usage pattern — is fully supported.
|
|
14
|
+
- Everything else the generics-monomorphization plan already scoped out (a method introducing its own additional generic parameter; trait-bound enforcement; `libs/std` compiling as-is) is unchanged.
|
|
15
|
+
- Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use the `"monomorphize: ..."` prefix.
|
|
16
|
+
- Must leave `cargo test --workspace` and `npx --yes tree-sitter-cli test` (from `tooling/tree-sitter-plum/`) green.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Task 1: Mangle enum variant names; rewrite construction and match-pattern references; remove the collision guard
|
|
21
|
+
|
|
22
|
+
**Files:**
|
|
23
|
+
- Modify: `plum-checker/src/monomorphize.rs`
|
|
24
|
+
- Modify: `plum-checker/tests/checker_tests.rs`
|
|
25
|
+
- Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
|
|
26
|
+
- Modify: `README.md`
|
|
27
|
+
|
|
28
|
+
**Interfaces:**
|
|
29
|
+
- Consumes: nothing new — this is entirely internal to the already-built `monomorphize.rs`.
|
|
30
|
+
- Produces: `resolve_enum_instantiation`'s signature changes from `(&mut self, call: &ast::FnCall, env: &TypeEnv)` to `(&mut self, call: &mut ast::FnCall, env: &TypeEnv)` (it now rewrites `call.name`, matching the shape `resolve_class_instantiation`/`resolve_fn_instantiation` already have). `Monomorphizer`'s `enum_variant_owner: BTreeMap<String, String>` field is replaced by `enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>` (mangled enum name -> {original variant name -> mangled variant name}). Nothing outside `plum-checker` depends on either name.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Write failing tests**
|
|
33
|
+
|
|
34
|
+
Read `plum-checker/tests/checker_tests.rs` around lines 389-466 first to confirm current line numbers still match (these are the two existing tests this step modifies/replaces).
|
|
35
|
+
|
|
36
|
+
Replace the existing `generic_enum_single_instantiation_type_checks` test's assertion (it currently asserts the variant name stays unmangled — that assertion is about to become wrong on purpose). Change:
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
let some = opt.variants.iter().find(|v| v.name == "Some")
|
|
40
|
+
.expect("expected `Some` variant on `Option$Int`");
|
|
41
|
+
assert_eq!(some.fields, vec!["Int".to_string()], "Some's field should be concrete Int");
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
to:
|
|
45
|
+
|
|
46
|
+
```rust
|
|
47
|
+
let some = opt.variants.iter().find(|v| v.name == "Some$Int")
|
|
48
|
+
.expect("expected `Some$Int` (mangled) variant on `Option$Int`");
|
|
49
|
+
assert_eq!(some.fields, vec!["Int".to_string()], "Some's field should be concrete Int");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Replace the entire `generic_enum_multi_instantiation_is_a_clear_error` test (currently asserting the old collision-error behavior, which this task removes) with a coexistence test:
|
|
53
|
+
|
|
54
|
+
```rust
|
|
55
|
+
#[test]
|
|
56
|
+
fn generic_enum_multiple_instantiations_coexist_and_type_check() {
|
|
57
|
+
// The SAME generic enum instantiated at two different concrete types in one
|
|
58
|
+
// program must now type-check correctly for BOTH instantiations — this is the
|
|
59
|
+
// behavior this task adds (previously this was a documented, rejected limitation).
|
|
60
|
+
let src = "\
|
|
61
|
+
enum Option =
|
|
62
|
+
| Some(a)
|
|
63
|
+
| None
|
|
64
|
+
|
|
65
|
+
useInt() -> Int =
|
|
66
|
+
o = Some(5)
|
|
67
|
+
match o
|
|
68
|
+
Some(v) =>
|
|
69
|
+
v
|
|
70
|
+
None =>
|
|
71
|
+
0
|
|
72
|
+
|
|
73
|
+
useStr() -> Str =
|
|
74
|
+
o = Some(\"x\")
|
|
75
|
+
match o
|
|
76
|
+
Some(v) =>
|
|
77
|
+
v
|
|
78
|
+
None =>
|
|
79
|
+
\"z\"
|
|
80
|
+
";
|
|
81
|
+
let source = parse(src);
|
|
82
|
+
let result = check_source(&source);
|
|
83
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
84
|
+
|
|
85
|
+
// Directly prove both specializations exist independently, with distinct
|
|
86
|
+
// mangled variant names, so neither collides with the other.
|
|
87
|
+
let mono = plum_checker::monomorphize::monomorphize_source(&source)
|
|
88
|
+
.expect("monomorphize should succeed");
|
|
89
|
+
let has_enum_with_variant = |enum_name: &str, variant_name: &str| {
|
|
90
|
+
mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == enum_name
|
|
91
|
+
&& e.variants.iter().any(|v| v.name == variant_name)))
|
|
92
|
+
};
|
|
93
|
+
assert!(has_enum_with_variant("Option$Int", "Some$Int"), "expected Option$Int with Some$Int");
|
|
94
|
+
assert!(has_enum_with_variant("Option$Str", "Some$Str"), "expected Option$Str with Some$Str");
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Read `plum-wasm-codegen/tests/codegen_tests.rs` around line 759 (`generic_enum_specialized_and_matched_runs_correctly`) for context, then append a new test right after it:
|
|
99
|
+
|
|
100
|
+
```rust
|
|
101
|
+
#[test]
|
|
102
|
+
fn generic_enum_multiple_instantiations_coexist_and_run_correctly() {
|
|
103
|
+
let src = "\
|
|
104
|
+
enum Option =
|
|
105
|
+
| Some(a)
|
|
106
|
+
| None
|
|
107
|
+
|
|
108
|
+
unwrapIntOr(o: Option, default: Int) -> Int =
|
|
109
|
+
match o
|
|
110
|
+
Some(v) =>
|
|
111
|
+
v
|
|
112
|
+
None =>
|
|
113
|
+
default
|
|
114
|
+
|
|
115
|
+
unwrapStrLenOr(o: Option, default: Int) -> Int =
|
|
116
|
+
match o
|
|
117
|
+
Some(v) =>
|
|
118
|
+
v.length()
|
|
119
|
+
None =>
|
|
120
|
+
default
|
|
121
|
+
|
|
122
|
+
main() -> Int =
|
|
123
|
+
unwrapIntOr(Some(13), 0) + unwrapStrLenOr(Some(\"abcd\"), 0)
|
|
124
|
+
";
|
|
125
|
+
let source = parse(src);
|
|
126
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
127
|
+
assert_eq!(run_main(&bytes), 17);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
(If `Str.length()` isn't a real, already-working method in this codebase — check `libs/std/str.plum` or existing string tests — substitute a different `Str`-typed operation you confirm already compiles, such as comparing the string against a literal and branching, so the test still exercises `Option$Str` meaningfully without depending on something unimplemented. Adjust the expected `main` result accordingly if you change the body.)
|
|
132
|
+
|
|
133
|
+
- [ ] **Step 2: Run to see them fail**
|
|
134
|
+
|
|
135
|
+
Run: `cargo test -p plum-checker --test checker_tests generic_enum` and `cargo test -p plum-wasm-codegen --test codegen_tests generic_enum`
|
|
136
|
+
Expected: `generic_enum_single_instantiation_type_checks` fails (variant is still named `Some`, not `Some$Int`); `generic_enum_multiple_instantiations_coexist_and_type_check` and its codegen counterpart fail with the old collision error (`"more than one concrete type"`).
|
|
137
|
+
|
|
138
|
+
- [ ] **Step 3: Read the current file, then make the edits**
|
|
139
|
+
|
|
140
|
+
Read `plum-checker/src/monomorphize.rs` in full first — Task/fix-round history means line numbers may have shifted slightly from what's shown below; verify each snippet still matches before replacing it.
|
|
141
|
+
|
|
142
|
+
**3a. Update `specialize_enum`** to mangle variant names:
|
|
143
|
+
|
|
144
|
+
Replace:
|
|
145
|
+
|
|
146
|
+
```rust
|
|
147
|
+
/// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
|
|
148
|
+
/// substituting every variant field type name that matches one of the enum's
|
|
149
|
+
/// generic parameters with its resolved concrete type's name.
|
|
150
|
+
pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
|
|
151
|
+
ast::Enum {
|
|
152
|
+
name: mangled_name.to_string(),
|
|
153
|
+
variants: e.variants.iter().map(|v| ast::EnumVariant {
|
|
154
|
+
name: v.name.clone(),
|
|
155
|
+
fields: v.fields.iter().map(|f| {
|
|
156
|
+
subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
|
|
157
|
+
}).collect(),
|
|
158
|
+
}).collect(),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
with:
|
|
164
|
+
|
|
165
|
+
```rust
|
|
166
|
+
/// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
|
|
167
|
+
/// substituting every variant field type name that matches one of the enum's
|
|
168
|
+
/// generic parameters with its resolved concrete type's name.
|
|
169
|
+
///
|
|
170
|
+
/// Variant names are ALSO mangled here, with the same suffix as the enum's own
|
|
171
|
+
/// name (e.g. `Some` -> `Some$Int`) — even a payload-free variant like `None`.
|
|
172
|
+
/// This is necessary because the runtime `EnumVariants` table (built by
|
|
173
|
+
/// `build_global_tables`) is keyed by bare variant name globally: without this,
|
|
174
|
+
/// two specializations of the same generic enum would both register a variant
|
|
175
|
+
/// literally named `Some`, colliding in that flat table.
|
|
176
|
+
pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
|
|
177
|
+
let params = enum_generic_params(e);
|
|
178
|
+
let type_args: Vec<PlumType> = params.iter().filter_map(|p| subst.get(p).cloned()).collect();
|
|
179
|
+
ast::Enum {
|
|
180
|
+
name: mangled_name.to_string(),
|
|
181
|
+
variants: e.variants.iter().map(|v| ast::EnumVariant {
|
|
182
|
+
name: mangle(&v.name, &type_args),
|
|
183
|
+
fields: v.fields.iter().map(|f| {
|
|
184
|
+
subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
|
|
185
|
+
}).collect(),
|
|
186
|
+
}).collect(),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**3b. Replace the `enum_variant_owner` field** on `Monomorphizer` with `enum_variant_mangling`:
|
|
192
|
+
|
|
193
|
+
Replace:
|
|
194
|
+
|
|
195
|
+
```rust
|
|
196
|
+
/// Bare variant name -> the mangled enum name that has currently "claimed" it.
|
|
197
|
+
/// Because the runtime `EnumVariants` table is keyed by BARE variant name
|
|
198
|
+
/// globally, two specializations of the same generic enum would both try to
|
|
199
|
+
/// register `"Some"`, silently colliding. We detect that here and error rather
|
|
200
|
+
/// than corrupt one specialization (single-instantiation-per-generic-enum is a
|
|
201
|
+
/// documented limitation of this pass).
|
|
202
|
+
enum_variant_owner: BTreeMap<String, String>,
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
with:
|
|
206
|
+
|
|
207
|
+
```rust
|
|
208
|
+
/// Mangled enum name -> {original variant name -> mangled variant name}, e.g.
|
|
209
|
+
/// `"Option$Int" -> {"Some": "Some$Int", "None": "None$Int"}`. Populated eagerly
|
|
210
|
+
/// (in `resolve_enum_instantiation`, at the moment an instantiation's concrete
|
|
211
|
+
/// type arguments become known) rather than waiting for the worklist to actually
|
|
212
|
+
/// produce that specialization — so both a construction call site and a later
|
|
213
|
+
/// `match` on the same specialization can rewrite variant names consistently,
|
|
214
|
+
/// regardless of processing order.
|
|
215
|
+
enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>,
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**3c. Replace `resolve_enum_instantiation`** entirely:
|
|
219
|
+
|
|
220
|
+
```rust
|
|
221
|
+
/// Resolves a construction of a generic enum's variant (e.g. `Some(5)` for
|
|
222
|
+
/// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
|
|
223
|
+
/// variant name (`Some`) to its mangled form (`Some$Int`) once the enum's own
|
|
224
|
+
/// concrete instantiation is known. Mangling is eager and deterministic — it
|
|
225
|
+
/// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
|
|
226
|
+
/// (see `enum_variant_mangling`'s doc comment).
|
|
227
|
+
///
|
|
228
|
+
/// A variant that carries no generic fields (e.g. `None`) can't pin down the
|
|
229
|
+
/// enum's type parameters on its own, so such a construction site is left alone
|
|
230
|
+
/// here — some other construction site (e.g. `Some(5)`) is what drives the
|
|
231
|
+
/// specialization. (A bare `None` used as a *value*, not a call, is
|
|
232
|
+
/// `ast::Expr::TypeName` and doesn't go through this function at all — see the
|
|
233
|
+
/// plan's Global Constraints for that narrower, documented residual limitation.)
|
|
234
|
+
fn resolve_enum_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
|
|
235
|
+
let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() else { return Ok(()) };
|
|
236
|
+
let params = enum_generic_params(e);
|
|
237
|
+
let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else { return Ok(()) };
|
|
238
|
+
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
|
|
239
|
+
for (field_ty_name, arg) in variant.fields.iter().zip(call.args.iter()) {
|
|
240
|
+
if params.contains(field_ty_name) {
|
|
241
|
+
let arg_expr = match arg {
|
|
242
|
+
ast::Arg::Positional(e) => e,
|
|
243
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
244
|
+
ast::Arg::Pair { value, .. } => value,
|
|
245
|
+
};
|
|
246
|
+
bindings.entry(field_ty_name.clone()).or_insert_with(|| self.infer(arg_expr, env));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// This single construction site couldn't pin down every generic parameter
|
|
250
|
+
// (e.g. a payload-free `None`, or a variant that mentions only some of a
|
|
251
|
+
// multi-parameter enum's params). Leave it for another site to drive.
|
|
252
|
+
if bindings.len() != params.len() {
|
|
253
|
+
return Ok(());
|
|
254
|
+
}
|
|
255
|
+
let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
|
|
256
|
+
let mangled = mangle(&e.name, &type_args);
|
|
257
|
+
|
|
258
|
+
self.enum_variant_mangling.entry(mangled.clone()).or_insert_with(|| {
|
|
259
|
+
e.variants.iter().map(|v| (v.name.clone(), mangle(&v.name, &type_args))).collect()
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
|
|
263
|
+
self.enqueued.insert(mangled.clone());
|
|
264
|
+
self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled: mangled.clone() });
|
|
265
|
+
}
|
|
266
|
+
call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
|
|
267
|
+
Ok(())
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**3d. Add match-pattern rewriting to `rewrite_stmt`'s `Match` arm**:
|
|
272
|
+
|
|
273
|
+
Replace:
|
|
274
|
+
|
|
275
|
+
```rust
|
|
276
|
+
ast::Stmt::Match(m) => {
|
|
277
|
+
for s in &mut m.subjects {
|
|
278
|
+
self.rewrite_expr(s, env)?;
|
|
279
|
+
}
|
|
280
|
+
let subject_ty = m.subjects.first().map(|s| self.infer(s, env)).unwrap_or(PlumType::TInt);
|
|
281
|
+
for case in &mut m.cases {
|
|
282
|
+
let mut case_env = env.clone();
|
|
283
|
+
if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
|
|
284
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
285
|
+
&& self.enum_variants.contains_key(n);
|
|
286
|
+
if !is_variant {
|
|
287
|
+
case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
self.rewrite_block(&mut case.body, &mut case_env)?;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
with:
|
|
296
|
+
|
|
297
|
+
```rust
|
|
298
|
+
ast::Stmt::Match(m) => {
|
|
299
|
+
for s in &mut m.subjects {
|
|
300
|
+
self.rewrite_expr(s, env)?;
|
|
301
|
+
}
|
|
302
|
+
let subject_ty = m.subjects.first().map(|s| self.infer(s, env)).unwrap_or(PlumType::TInt);
|
|
303
|
+
// If the subject's concrete type is a specialized generic enum, its
|
|
304
|
+
// variant-name mangling table lets us rewrite this match's patterns
|
|
305
|
+
// (`Some`/`None` -> `Some$Int`/`None$Int`) to reference the correct
|
|
306
|
+
// specialization, so the checker/codegen's unmodified, bare-name-keyed
|
|
307
|
+
// `EnumVariants` lookup still resolves each pattern correctly.
|
|
308
|
+
let variant_mangling: Option<BTreeMap<String, String>> = match &subject_ty {
|
|
309
|
+
PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
|
|
310
|
+
_ => None,
|
|
311
|
+
};
|
|
312
|
+
for case in &mut m.cases {
|
|
313
|
+
let mut case_env = env.clone();
|
|
314
|
+
if let Some(pat) = case.patterns.first_mut() {
|
|
315
|
+
match pat {
|
|
316
|
+
ast::CasePattern::Name(n) => {
|
|
317
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
318
|
+
&& self.enum_variants.contains_key(n.as_str());
|
|
319
|
+
if is_variant {
|
|
320
|
+
if let Some(table) = &variant_mangling {
|
|
321
|
+
if let Some(mangled_variant) = table.get(n) {
|
|
322
|
+
*n = mangled_variant.clone();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
ast::CasePattern::Class { name, .. } => {
|
|
330
|
+
if let Some(table) = &variant_mangling {
|
|
331
|
+
if let Some(mangled_variant) = table.get(name) {
|
|
332
|
+
*name = mangled_variant.clone();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
_ => {}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
self.rewrite_block(&mut case.body, &mut case_env)?;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**3e. Update the `rewrite_expr`'s `FnCall` arm's comment** (the code itself is unchanged — `resolve_enum_instantiation` is already called before `resolve_fn_instantiation` — only the stale comment claiming enum resolution "never rewrites `call.name`" needs correcting):
|
|
345
|
+
|
|
346
|
+
Replace:
|
|
347
|
+
|
|
348
|
+
```rust
|
|
349
|
+
// A `FnCall` may name either a generic free function or a generic
|
|
350
|
+
// enum's variant; the two name spaces don't overlap (variants are
|
|
351
|
+
// capitalized), so checking both is safe. Enum resolution never
|
|
352
|
+
// rewrites `call.name`, so order doesn't matter.
|
|
353
|
+
self.resolve_enum_instantiation(call, env)?;
|
|
354
|
+
self.resolve_fn_instantiation(call, env)?;
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
with:
|
|
358
|
+
|
|
359
|
+
```rust
|
|
360
|
+
// A `FnCall` may name either a generic free function or a generic
|
|
361
|
+
// enum's variant; the two name spaces don't overlap (variants are
|
|
362
|
+
// capitalized). Enum resolution runs first and rewrites `call.name`
|
|
363
|
+
// to its mangled form when it resolves — `fns_generic` is keyed by
|
|
364
|
+
// the ORIGINAL unmangled free-function names, so a rewritten variant
|
|
365
|
+
// name can never accidentally match it afterward.
|
|
366
|
+
self.resolve_enum_instantiation(call, env)?;
|
|
367
|
+
self.resolve_fn_instantiation(call, env)?;
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**3f. Update `monomorphize_source`'s `Monomorphizer` struct literal**:
|
|
371
|
+
|
|
372
|
+
Replace:
|
|
373
|
+
|
|
374
|
+
```rust
|
|
375
|
+
enum_variant_owner: BTreeMap::new(),
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
with:
|
|
379
|
+
|
|
380
|
+
```rust
|
|
381
|
+
enum_variant_mangling: BTreeMap::new(),
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**3g. Simplify the worklist's `PendingSpecialization::Enum` arm** — remove the collision-detection loop entirely:
|
|
385
|
+
|
|
386
|
+
Replace:
|
|
387
|
+
|
|
388
|
+
```rust
|
|
389
|
+
PendingSpecialization::Enum { base, subst, mangled } => {
|
|
390
|
+
if !m.specialized.insert(mangled.clone()) { continue; }
|
|
391
|
+
let spec_enum = specialize_enum(base, &subst, &mangled);
|
|
392
|
+
// Claim each bare variant name for this mangled enum. If a DIFFERENT
|
|
393
|
+
// mangled enum already owns it, this generic enum is being
|
|
394
|
+
// instantiated at more than one concrete type in the same program —
|
|
395
|
+
// which the flat, bare-variant-name-keyed `EnumVariants` runtime
|
|
396
|
+
// table can't represent (both would register under `"Some"`). Rather
|
|
397
|
+
// than silently let the second specialization corrupt the first, we
|
|
398
|
+
// fail with a clear, specific error. (Re-claiming by the SAME mangled
|
|
399
|
+
// enum can't reach here — worklist dedup + the `specialized` guard
|
|
400
|
+
// above ensure each mangled enum is produced exactly once.)
|
|
401
|
+
for v in &spec_enum.variants {
|
|
402
|
+
if let Some(owner) = m.enum_variant_owner.get(&v.name) {
|
|
403
|
+
if owner != &mangled {
|
|
404
|
+
return Err(format!(
|
|
405
|
+
"monomorphize: generic enum '{}' is instantiated at more than one concrete type in the same program ('{}' and '{}'), which is not yet supported. Only a single concrete instantiation per generic enum is allowed (variant '{}' would collide in the global variant table). This is a known, documented limitation, not a bug.",
|
|
406
|
+
base.name, owner, mangled, v.name
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
m.enum_variant_owner.insert(v.name.clone(), mangled.clone());
|
|
411
|
+
}
|
|
412
|
+
m.produced.push(ast::Item::Enum(spec_enum));
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
with:
|
|
417
|
+
|
|
418
|
+
```rust
|
|
419
|
+
PendingSpecialization::Enum { base, subst, mangled } => {
|
|
420
|
+
if !m.specialized.insert(mangled.clone()) { continue; }
|
|
421
|
+
let spec_enum = specialize_enum(base, &subst, &mangled);
|
|
422
|
+
m.produced.push(ast::Item::Enum(spec_enum));
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
(`base` is already available directly as the arm's own field — no re-lookup needed. This is the same dedup-and-produce shape as `PendingSpecialization::Class`/`Fn`, just without the class/fn-specific signature-registration bookkeeping those arms also do, since nothing looks up an enum's "signature" the way a call site looks up a function's.)
|
|
427
|
+
|
|
428
|
+
- [ ] **Step 4: Run the checker and codegen tests**
|
|
429
|
+
|
|
430
|
+
Run: `cargo test -p plum-checker --test checker_tests generic_enum`
|
|
431
|
+
Expected: both pass.
|
|
432
|
+
|
|
433
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests generic_enum`
|
|
434
|
+
Expected: both pass, including the pre-existing `generic_enum_specialized_and_matched_runs_correctly` (single instantiation) — confirm it still passes unchanged, proving this change doesn't regress the already-working single-instantiation case.
|
|
435
|
+
|
|
436
|
+
- [ ] **Step 5: Update README**
|
|
437
|
+
|
|
438
|
+
In `README.md`, replace the sentence (currently around line 243):
|
|
439
|
+
|
|
440
|
+
```markdown
|
|
441
|
+
One documented limitation: a generic *enum* may only be instantiated at one concrete type per program (instantiating the same generic enum at two different concrete types produces a clear `monomorphize:`-prefixed error, since the runtime's enum-variant table is keyed by bare variant name).
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
with:
|
|
445
|
+
|
|
446
|
+
```markdown
|
|
447
|
+
Generic enums support any number of concrete instantiations coexisting in one program (variant names are mangled per instantiation, e.g. `Some` -> `Some$Int`/`Some$Str`, internally — invisible to user code). One narrower residual limitation: a payload-free variant (e.g. `None`) used as a bare value *outside* of a `match` pattern can't be disambiguated between multiple concrete instantiations of its enum from that expression alone; constructing via a payload-carrying sibling (`Some(5)`) and matching (`Some(v) => ...`, `None => ...`) is fully supported and is the overwhelmingly common usage pattern.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 6: Run the full workspace and tree-sitter suites**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
cargo test --workspace
|
|
454
|
+
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Expected: fully green, zero known failures (aside from the pre-existing, intentionally-`#[ignore]`d slow recursion-guard test, which is unaffected by this change and can be skipped by default).
|
|
458
|
+
|
|
459
|
+
- [ ] **Step 7: Commit**
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
git add plum-checker/src/monomorphize.rs plum-checker/tests/checker_tests.rs plum-wasm-codegen/tests/codegen_tests.rs README.md
|
|
463
|
+
git commit -m "feat(plum-checker): support multiple concrete instantiations of the same generic enum"
|
|
464
|
+
```
|