plum

#treesitter#compiler#wasm

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

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


e698d53Peter John 2026-07-20T20:06:27+05:30
docs: revise generic-enum plan with bare-generic-typed-param fix as Task 1
docs/superpowers/plans/2026-07-20-generic-enum-multi-instantiation.md CHANGED
@@ -6,434 +6,373 @@
6
6
 
7
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
8
 
9
+ **Important — current repository state:** the checker-side half of variant mangling (what was originally this plan's only task) is **already implemented, correct, and sitting uncommitted** in `plum-checker/src/monomorphize.rs`/`plum-checker/tests/checker_tests.rs`/`plum-wasm-codegen/tests/codegen_tests.rs` — do not revert or redo it. It was blocked from being committed by a real gap discovered during implementation (see Task 1 below), which must land first. Read the current state of `plum-checker/src/monomorphize.rs` before starting — it already contains `enum_variant_mangling`, the rewritten `resolve_enum_instantiation`, and `rewrite_stmt`'s `Match`-pattern rewriting.
10
+
9
11
  **Tech Stack:** Rust (`plum-checker` crate only for this plan — `plum-core`/`plum-wasm-codegen` need no changes).
10
12
 
11
13
  ## Global Constraints
12
14
 
13
15
  - **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.
16
+ - **A method** (not a free function) introducing this same bare-generic-reference shape (a method on a non-generic class whose own param bare-names a generic class/enum) is out of scope for this plan, consistent with the existing "a method introducing its own additional generic parameter" limitation.
17
+ - A function that is simultaneously truly-generic (lowercase-letter params) *and* bare-references another generic type is out of scope (no current example needs it).
18
+ - Everything else the generics-monomorphization plan already scoped out (trait-bound enforcement; `libs/std` compiling as-is) is unchanged.
15
19
  - Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use the `"monomorphize: ..."` prefix.
16
20
  - Must leave `cargo test --workspace` and `npx --yes tree-sitter-cli test` (from `tooling/tree-sitter-plum/`) green.
17
21
 
18
22
  ---
19
23
 
20
- ### Task 1: Mangle enum variant names; rewrite construction and match-pattern references; remove the collision guard
24
+ ### Task 1: Specialize ordinary functions with a bare generic-class/enum-typed parameter
21
25
 
22
26
  **Files:**
23
27
  - Modify: `plum-checker/src/monomorphize.rs`
24
28
  - Modify: `plum-checker/tests/checker_tests.rs`
25
29
  - Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
26
- - Modify: `README.md`
30
+
31
+ **Why this task exists:** implementing the (already-uncommitted) variant-mangling work surfaced a real, blocking gap: an *ordinary* function (no lowercase-letter generic params) that takes a bare generic-enum-typed parameter — the completely normal way to write this, e.g. `unwrapOr(o: Option, default: Int) -> Int`, the shape the **pre-existing** `generic_enum_specialized_and_matched_runs_correctly` codegen test already uses — never has that param's type resolved to a concrete specialization at all. `o`'s declared type stays the literal, unmangled `Option`; `match o` inside its body then infers a subject type of `TNamed("Option")`, a key that can never match the mangling table (keyed by `"Option$Int"`). This isn't an ordering bug — verified directly (including by reordering source) — the *key itself* never matches, no matter when discovery happens. The same root cause affects generic **classes** for the identical param shape (a bare `Box`-typed parameter), untested until now only because no prior test happened to exercise it.
27
32
 
28
33
  **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.
34
+ - Consumes: `enum_generic_params`, `class_generic_params`, `mangle`, `Substitution`, `specialize_fn` (all already exist, unchanged).
35
+ - Produces: two new `Monomorphizer` fields `enums_generic_by_name: BTreeMap<String, &'a ast::Enum>` (keyed by the enum's own name, distinct from the existing `enums_generic_by_variant` keyed by variant name) and `fns_bare_generic: BTreeMap<String, &'a ast::Fn>` (free functions needing this new kind of specialization). A new method `Monomorphizer::fn_bare_generic_refs` and a new method `Monomorphizer::resolve_bare_generic_fn_instantiation`. Reuses the existing `PendingSpecialization::Fn` worklist variant unchanged — no new variant needed.
31
36
 
32
37
  - [ ] **Step 1: Write failing tests**
33
38
 
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:
39
+ Append to `plum-checker/tests/checker_tests.rs`:
53
40
 
54
41
  ```rust
55
42
  #[test]
56
- fn generic_enum_multiple_instantiations_coexist_and_type_check() {
43
+ fn ordinary_function_with_bare_generic_enum_param_type_checks() {
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).
44
+ // The shape that broke the pre-existing codegen test: an otherwise-ordinary
45
+ // function taking a bare generic-enum-typed parameter.
60
46
  let src = "\
61
47
  enum Option =
62
48
  | Some(a)
63
49
  | None
64
50
 
65
- useInt() -> Int =
51
+ unwrapOr(o: Option, default: Int) -> Int =
66
- o = Some(5)
67
52
  match o
68
53
  Some(v) =>
69
54
  v
70
55
  None =>
71
- 0
56
+ default
72
57
 
73
- useStr() -> Str =
58
+ use() -> Int =
74
- o = Some(\"x\")
75
- match o
76
- Some(v) =>
59
+ unwrapOr(Some(5), 0)
77
- v
78
- None =>
79
- \"z\"
80
60
  ";
81
61
  let source = parse(src);
82
62
  let result = check_source(&source);
83
63
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
84
64
 
85
- // Directly prove both specializations exist independently, with distinct
65
+ // Directly prove `unwrapOr` itself got specialized (not left bare/unresolved).
86
- // mangled variant names, so neither collides with the other.
87
66
  let mono = plum_checker::monomorphize::monomorphize_source(&source)
88
67
  .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
68
+ let has_specialized_unwrap_or = mono.items.iter().any(|it| matches!(it, Item::Fn(f)
91
- && e.variants.iter().any(|v| v.name == variant_name)))
69
+ if f.name.starts_with("unwrapOr$") && f.type_param.is_none()));
70
+ assert!(has_specialized_unwrap_or, "expected a specialized `unwrapOr$...` function in the output");
92
- };
71
+ }
72
+
73
+ #[test]
74
+ fn ordinary_function_with_bare_generic_class_param_type_checks() {
75
+ // The same shape, for a generic CLASS param instead of an enum — untested until
93
- assert!(has_enum_with_variant("Option$Int", "Some$Int"), "expected Option$Int with Some$Int");
76
+ // now, but the identical root cause: `Box` is dropped from the monomorphized
77
+ // output, so a bare `Box`-typed param would otherwise reference nothing.
78
+ let src = "\
79
+ type Box(a) =
80
+ value: a
81
+
82
+ getBoxValue<Box>() -> a =
83
+ self.value
84
+
85
+ sumBox(b: Box) -> Int =
86
+ b.getBoxValue()
87
+
88
+ use() -> Int =
89
+ sumBox(Box(value: 5))
90
+ ";
91
+ let source = parse(src);
92
+ let result = check_source(&source);
94
- assert!(has_enum_with_variant("Option$Str", "Some$Str"), "expected Option$Str with Some$Str");
93
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
95
94
  }
96
95
  ```
97
96
 
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:
97
+ Read `plum-wasm-codegen/tests/codegen_tests.rs`'s existing `generic_enum_specialized_and_matched_runs_correctly` test (search for it) for its exact current shape — this task must not change that test, but should confirm (Step 4) that it now passes without modification. Then append a new test proving the class-side fix works end-to-end via wasm execution:
99
98
 
100
99
  ```rust
101
100
  #[test]
102
- fn generic_enum_multiple_instantiations_coexist_and_run_correctly() {
101
+ fn ordinary_function_with_bare_generic_class_param_runs_correctly() {
103
102
  let src = "\
104
- enum Option =
103
+ type Box(a) =
105
- | Some(a)
104
+ value: a
106
- | None
107
105
 
108
- unwrapIntOr(o: Option, default: Int) -> Int =
106
+ getBoxValue<Box>() -> Int =
109
- match o
110
- Some(v) =>
111
- v
112
- None =>
113
- default
107
+ self.value
114
108
 
115
- unwrapStrLenOr(o: Option, default: Int) -> Int =
109
+ sumBox(b: Box) -> Int =
116
- match o
117
- Some(v) =>
118
- v.length()
110
+ b.getBoxValue()
119
- None =>
120
- default
121
111
 
122
112
  main() -> Int =
123
- unwrapIntOr(Some(13), 0) + unwrapStrLenOr(Some(\"abcd\"), 0)
113
+ sumBox(Box(value: 11))
124
114
  ";
125
115
  let source = parse(src);
126
116
  let bytes = compile_source(&source).expect("compile failed");
127
- assert_eq!(run_main(&bytes), 17);
117
+ assert_eq!(run_main(&bytes), 11);
128
118
  }
129
119
  ```
130
120
 
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
121
  - [ ] **Step 2: Run to see them fail**
134
122
 
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"`).
123
+ Run: `cargo test -p plum-checker --test checker_tests ordinary_function_with_bare`
124
+ Expected: both fail `check_source` returns an error (the bare `Option`/`Box` param never resolves).
137
125
 
138
- - [ ] **Step 3: Read the current file, then make the edits**
126
+ Run: `cargo test -p plum-wasm-codegen --test codegen_tests generic_enum_specialized_and_matched_runs_correctly ordinary_function_with_bare_generic_class_param_runs_correctly`
127
+ Expected: both fail — this is the actual blocker (`generic_enum_specialized_and_matched_runs_correctly` is the pre-existing test, currently broken by the already-uncommitted variant-mangling changes; the new class-param test fails for the analogous reason).
139
128
 
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.
129
+ - [ ] **Step 3: Read the current file, then make the edits**
141
130
 
142
- **3a. Update `specialize_enum`** to mangle variant names:
131
+ Read `plum-checker/src/monomorphize.rs` in full first — it already contains the uncommitted variant-mangling work; verify each snippet below still matches before replacing it.
143
132
 
144
- Replace:
133
+ **3a. Add the two new `Monomorphizer` fields.** Find the struct definition (it currently has `enums_generic_by_variant` and `enum_variant_mangling` fields among others) and add, alongside them:
145
134
 
146
135
  ```rust
147
- /// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
136
+ /// The enum's own bare name -> the generic `Enum` — used to detect a bare
137
+ /// generic-enum-typed function param (e.g. `o: Option`), distinct from
138
+ /// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
139
+ /// sites like `Some(5)`).
140
+ enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
141
+ /// Free functions that are NOT generic by `fn_generic_params`'s lowercase-letter
148
- /// substituting every variant field type name that matches one of the enum's
142
+ /// convention, but whose param type(s) bare-name a generic class or enum (e.g.
143
+ /// `unwrapOr(o: Option, ...)`) — such a function still needs its own
149
- /// generic parameters with its resolved concrete type's name.
144
+ /// per-call-site specialization, since its receiver generic class/enum is
150
- pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
145
+ /// dropped from the monomorphized output and the bare name would otherwise
151
- ast::Enum {
152
- name: mangled_name.to_string(),
146
+ /// resolve to nothing.
153
- variants: e.variants.iter().map(|v| ast::EnumVariant {
147
+ fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
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
148
  ```
162
149
 
163
- with:
150
+ **3b. Add `fn_bare_generic_refs` and `resolve_bare_generic_fn_instantiation` methods** to `impl<'a> Monomorphizer<'a>`, anywhere alongside the other `resolve_*_instantiation` methods:
164
151
 
165
152
  ```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
153
+ /// The bare names of any generic class or enum referenced directly (not via a
154
+ /// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Option"` for
155
+ /// `unwrapOr(o: Option, default: Int) -> Int`. See `fns_bare_generic`'s doc
175
- /// literally named `Some`, colliding in that flat table.
156
+ /// comment for why such a function needs its own specialization.
176
- pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
157
+ fn fn_bare_generic_refs(&self, f: &ast::Fn) -> Vec<String> {
177
- let params = enum_generic_params(e);
158
+ let mut names: Vec<String> = Vec::new();
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(),
159
+ for p in &f.params {
181
- variants: e.variants.iter().map(|v| ast::EnumVariant {
160
+ let n = match &p.ty {
182
- name: mangle(&v.name, &type_args),
161
+ ast::ParamType::Type(t) => &t.name,
162
+ ast::ParamType::Variadic(t) => &t.name,
163
+ };
164
+ if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
183
- fields: v.fields.iter().map(|f| {
165
+ && !names.iter().any(|x| x == n)
184
- subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
166
+ {
185
- }).collect(),
167
+ names.push(n.clone());
168
+ }
169
+ }
186
- }).collect(),
170
+ names
187
171
  }
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
172
 
220
- ```rust
221
- /// Resolves a construction of a generic enum's variant (e.g. `Some(5)` for
173
+ /// Resolves a call to an otherwise-ordinary function whose param type(s)
222
- /// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
174
+ /// bare-name a generic class/enum, specializing it per call site exactly like a
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
175
+ /// truly-generic function reusing the same `PendingSpecialization::Fn`
176
+ /// worklist entry and the unmodified `specialize_fn`, whose substitution
177
+ /// mechanism already replaces any type whose bare name matches a substitution
225
- /// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
178
+ /// key (it doesn't care whether that key came from a lowercase-letter generic
226
- /// (see `enum_variant_mangling`'s doc comment).
179
+ /// parameter or a bare generic class/enum reference).
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> {
180
+ fn resolve_bare_generic_fn_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(()) };
181
+ let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
236
- let params = enum_generic_params(e);
182
+ let refs = self.fn_bare_generic_refs(f);
237
- let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else { return Ok(()) };
238
183
  let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
239
- for (field_ty_name, arg) in variant.fields.iter().zip(call.args.iter()) {
184
+ for (param, arg) in f.params.iter().zip(call.args.iter()) {
185
+ let n = match &param.ty {
186
+ ast::ParamType::Type(t) => t.name.clone(),
187
+ ast::ParamType::Variadic(t) => t.name.clone(),
188
+ };
240
- if params.contains(field_ty_name) {
189
+ if refs.contains(&n) {
241
190
  let arg_expr = match arg {
242
191
  ast::Arg::Positional(e) => e,
243
192
  ast::Arg::Keyword { value, .. } => value,
244
193
  ast::Arg::Pair { value, .. } => value,
245
194
  };
246
- bindings.entry(field_ty_name.clone()).or_insert_with(|| self.infer(arg_expr, env));
195
+ bindings.entry(n).or_insert_with(|| self.infer(arg_expr, env));
247
196
  }
248
197
  }
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() {
198
+ if bindings.len() != refs.len() {
253
- return Ok(());
199
+ return Err(format!(
200
+ "monomorphize: could not resolve all generic parameters for '{}' at this call site",
201
+ call.name
202
+ ));
254
203
  }
255
- let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
204
+ let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
256
- let mangled = mangle(&e.name, &type_args);
205
+ let mangled = mangle(&call.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
206
  if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
263
207
  self.enqueued.insert(mangled.clone());
264
- self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled: mangled.clone() });
208
+ self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
265
209
  }
266
- call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
210
+ call.name = mangled;
267
211
  Ok(())
268
212
  }
269
213
  ```
270
214
 
271
- **3d. Add match-pattern rewriting to `rewrite_stmt`'s `Match` arm**:
215
+ **3c. Wire the new resolution into `rewrite_expr`'s `FnCall` arm.** Find:
272
216
 
217
+ ```rust
218
+ self.resolve_enum_instantiation(call, env)?;
219
+ self.resolve_fn_instantiation(call, env)?;
220
+ ```
221
+
273
- Replace:
222
+ and change to:
274
223
 
275
224
  ```rust
276
- ast::Stmt::Match(m) => {
277
- for s in &mut m.subjects {
225
+ self.resolve_enum_instantiation(call, env)?;
278
- self.rewrite_expr(s, env)?;
226
+ self.resolve_fn_instantiation(call, 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)?;
227
+ self.resolve_bare_generic_fn_instantiation(call, env)?;
291
- }
292
- }
293
228
  ```
294
229
 
295
- with:
230
+ **3d. Extend `maybe_rewrite_return` to also recognize a bare generic *enum* return** (it already recognizes a bare generic *class* return). Find:
296
231
 
297
232
  ```rust
298
- ast::Stmt::Match(m) => {
233
+ Some(rt) => {
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 {
234
+ is_generic_param_name(&rt.name)
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());
235
+ || self.classes_generic.contains_key(&rt.name)
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
236
  }
342
237
  ```
343
238
 
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:
239
+ and change to:
347
240
 
348
241
  ```rust
349
- // A `FnCall` may name either a generic free function or a generic
242
+ Some(rt) => {
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.
243
+ is_generic_param_name(&rt.name)
353
- self.resolve_enum_instantiation(call, env)?;
244
+ || self.classes_generic.contains_key(&rt.name)
354
- self.resolve_fn_instantiation(call, env)?;
245
+ || self.enums_generic_by_name.contains_key(&rt.name)
246
+ }
355
247
  ```
356
248
 
357
- with:
249
+ **3e. Populate `enums_generic_by_name` and classify `fns_bare_generic`** in `monomorphize_source`. Find the first classification loop (it currently populates `classes_generic` and, for each generic enum, loops over its variants to populate `enums_generic_by_variant`):
358
250
 
359
251
  ```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)?;
252
+ for item in &source.items {
253
+ match item {
254
+ ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
255
+ ast::Item::Enum(e) if !enum_generic_params(e).is_empty() => {
256
+ for v in &e.variants {
367
- self.resolve_fn_instantiation(call, env)?;
257
+ m.enums_generic_by_variant.insert(v.name.clone(), e);
258
+ }
259
+ }
260
+ _ => {}
261
+ }
262
+ }
368
263
  ```
369
264
 
370
- **3f. Update `monomorphize_source`'s `Monomorphizer` struct literal**:
265
+ Change the `Enum` arm to also populate `enums_generic_by_name`:
371
-
372
- Replace:
373
266
 
374
267
  ```rust
268
+ for item in &source.items {
269
+ match item {
270
+ ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
271
+ ast::Item::Enum(e) if !enum_generic_params(e).is_empty() => {
272
+ m.enums_generic_by_name.insert(e.name.clone(), e);
273
+ for v in &e.variants {
375
- enum_variant_owner: BTreeMap::new(),
274
+ m.enums_generic_by_variant.insert(v.name.clone(), e);
275
+ }
276
+ }
277
+ _ => {}
278
+ }
279
+ }
376
280
  ```
377
281
 
378
- with:
282
+ Find the second loop (classifies `Fn` items into `methods_generic_on`/`fns_generic`):
379
283
 
380
284
  ```rust
285
+ for item in &source.items {
286
+ if let ast::Item::Fn(f) = item {
287
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
288
+ if receiver_is_generic {
289
+ m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
290
+ } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
381
- enum_variant_mangling: BTreeMap::new(),
291
+ m.fns_generic.insert(f.name.clone(), f);
292
+ }
293
+ // A method whose receiver is NOT generic is left as a regular method below,
294
+ // even if its own params/return happen to use a bare lowercase-letter type
295
+ // name — that shape (a method introducing its own extra generic parameter)
296
+ // is out of scope for this pass; see the plan's Global Constraints.
297
+ }
298
+ }
382
299
  ```
383
300
 
384
- **3g. Simplify the worklist's `PendingSpecialization::Enum` arm** — remove the collision-detection loop entirely:
301
+ Add a third `else if` branch:
385
302
 
386
- Replace:
303
+ ```rust
304
+ for item in &source.items {
305
+ if let ast::Item::Fn(f) = item {
306
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
307
+ if receiver_is_generic {
308
+ m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
309
+ } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
310
+ m.fns_generic.insert(f.name.clone(), f);
311
+ } else if f.type_param.is_none() && !m.fn_bare_generic_refs(f).is_empty() {
312
+ m.fns_bare_generic.insert(f.name.clone(), f);
313
+ }
314
+ // A method whose receiver is NOT generic is left as a regular method below,
315
+ // even if its own params/return happen to use a bare lowercase-letter type
316
+ // name, or bare-name a generic class/enum — those shapes are out of scope
317
+ // for this pass; see the plan's Global Constraints.
318
+ }
319
+ }
320
+ ```
321
+
322
+ **3f. Exclude `fns_bare_generic` members from direct pass-through.** Find the third loop's `Fn` arm:
387
323
 
388
324
  ```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());
325
+ ast::Item::Fn(f) => {
326
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
327
+ let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
328
+ if !receiver_is_generic && !is_generic_fn {
329
+ let mut f2 = f.clone();
330
+ m.rewrite_fn_body(&mut f2, false)?;
331
+ m.produced.push(ast::Item::Fn(f2));
411
332
  }
412
- m.produced.push(ast::Item::Enum(spec_enum));
413
333
  }
414
334
  ```
415
335
 
416
- with:
336
+ and change to:
417
337
 
418
338
  ```rust
419
- PendingSpecialization::Enum { base, subst, mangled } => {
339
+ ast::Item::Fn(f) => {
420
- if !m.specialized.insert(mangled.clone()) { continue; }
340
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
421
- let spec_enum = specialize_enum(base, &subst, &mangled);
341
+ let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
342
+ let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
343
+ if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
344
+ let mut f2 = f.clone();
345
+ m.rewrite_fn_body(&mut f2, false)?;
422
- m.produced.push(ast::Item::Enum(spec_enum));
346
+ m.produced.push(ast::Item::Fn(f2));
347
+ }
423
348
  }
424
349
  ```
425
350
 
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.)
351
+ **3g. Initialize the two new fields** in the `Monomorphizer` struct literal. Find:
352
+
353
+ ```rust
354
+ enums_generic_by_variant: BTreeMap::new(),
355
+ enum_variant_mangling: BTreeMap::new(),
356
+ ```
357
+
358
+ and change to:
427
359
 
360
+ ```rust
361
+ enums_generic_by_variant: BTreeMap::new(),
362
+ enums_generic_by_name: BTreeMap::new(),
363
+ enum_variant_mangling: BTreeMap::new(),
364
+ fns_bare_generic: BTreeMap::new(),
365
+ ```
366
+
428
- - [ ] **Step 4: Run the checker and codegen tests**
367
+ - [ ] **Step 4: Run all the tests**
429
368
 
430
- Run: `cargo test -p plum-checker --test checker_tests generic_enum`
431
- Expected: both pass.
369
+ Run: `cargo test -p plum-checker --test checker_tests`
370
+ Expected: fully green, including the two new tests and the two pre-existing enum tests (`generic_enum_single_instantiation_type_checks`, `generic_enum_multiple_instantiations_coexist_and_type_check`) from the already-uncommitted work.
432
371
 
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.
372
+ Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
373
+ Expected: fully green, including — critically — the **pre-existing** `generic_enum_specialized_and_matched_runs_correctly` passing unchanged (proving this task's fix resolves the actual blocker), the new `ordinary_function_with_bare_generic_class_param_runs_correctly`, and the already-uncommitted `generic_enum_multiple_instantiations_coexist_and_run_correctly`.
435
374
 
436
- - [ ] **Step 5: Update README**
375
+ - [ ] **Step 5: Update README (this was deferred by the blocked prior attempt)**
437
376
 
438
377
  In `README.md`, replace the sentence (currently around line 243):
439
378
 
@@ -454,10 +393,12 @@ cargo test --workspace
454
393
  cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
455
394
  ```
456
395
 
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).
396
+ Expected: fully green (aside from the pre-existing, intentionally-`#[ignore]`d slow recursion-guard test, unaffected by this change).
458
397
 
459
398
  - [ ] **Step 7: Commit**
460
399
 
400
+ This commit includes BOTH the already-uncommitted variant-mangling changes and this task's new bare-generic-ref specialization mechanism — they were never separately commit-able, since the mangling work only actually works once this task's fix lands.
401
+
461
402
  ```bash
462
403
  git add plum-checker/src/monomorphize.rs plum-checker/tests/checker_tests.rs plum-wasm-codegen/tests/codegen_tests.rs README.md
463
404
  git commit -m "feat(plum-checker): support multiple concrete instantiations of the same generic enum"