plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
e698d53
— Peter 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
|
-
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
30
|
-
- Produces: `
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
default
|
|
72
57
|
|
|
73
|
-
|
|
58
|
+
use() -> Int =
|
|
74
|
-
o = Some(\"x\")
|
|
75
|
-
match o
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
68
|
+
let has_specialized_unwrap_or = mono.items.iter().any(|it| matches!(it, Item::Fn(f)
|
|
91
|
-
|
|
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
|
-
|
|
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!(
|
|
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`
|
|
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
|
|
101
|
+
fn ordinary_function_with_bare_generic_class_param_runs_correctly() {
|
|
103
102
|
let src = "\
|
|
104
|
-
|
|
103
|
+
type Box(a) =
|
|
105
|
-
|
|
104
|
+
value: a
|
|
106
|
-
| None
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
getBoxValue<Box>() -> Int =
|
|
109
|
-
match o
|
|
110
|
-
Some(v) =>
|
|
111
|
-
v
|
|
112
|
-
None =>
|
|
113
|
-
|
|
107
|
+
self.value
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
sumBox(b: Box) -> Int =
|
|
116
|
-
match o
|
|
117
|
-
Some(v) =>
|
|
118
|
-
|
|
110
|
+
b.getBoxValue()
|
|
119
|
-
None =>
|
|
120
|
-
default
|
|
121
111
|
|
|
122
112
|
main() -> Int =
|
|
123
|
-
|
|
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),
|
|
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
|
|
136
|
-
Expected:
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
- [ ] **Step 3: Read the current file, then make the edits**
|
|
141
130
|
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
///
|
|
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
|
-
///
|
|
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
|
-
///
|
|
144
|
+
/// per-call-site specialization, since its receiver generic class/enum is
|
|
150
|
-
|
|
145
|
+
/// dropped from the monomorphized output and the bare name would otherwise
|
|
151
|
-
ast::Enum {
|
|
152
|
-
|
|
146
|
+
/// resolve to nothing.
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
///
|
|
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
|
-
///
|
|
156
|
+
/// comment for why such a function needs its own specialization.
|
|
176
|
-
|
|
157
|
+
fn fn_bare_generic_refs(&self, f: &ast::Fn) -> Vec<String> {
|
|
177
|
-
|
|
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
|
-
|
|
159
|
+
for p in &f.params {
|
|
181
|
-
|
|
160
|
+
let n = match &p.ty {
|
|
182
|
-
|
|
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
|
-
|
|
165
|
+
&& !names.iter().any(|x| x == n)
|
|
184
|
-
|
|
166
|
+
{
|
|
185
|
-
|
|
167
|
+
names.push(n.clone());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
186
|
-
|
|
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
|
|
173
|
+
/// Resolves a call to an otherwise-ordinary function whose param type(s)
|
|
222
|
-
///
|
|
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
|
-
///
|
|
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
|
|
178
|
+
/// key (it doesn't care whether that key came from a lowercase-letter generic
|
|
226
|
-
///
|
|
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
|
|
180
|
+
fn resolve_bare_generic_fn_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
|
|
235
|
-
let Some(
|
|
181
|
+
let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
|
|
236
|
-
let
|
|
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 (
|
|
184
|
+
for (param, arg) in f.params.iter().zip(call.args.iter()) {
|
|
185
|
+
let n = match ¶m.ty {
|
|
186
|
+
ast::ParamType::Type(t) => t.name.clone(),
|
|
187
|
+
ast::ParamType::Variadic(t) => t.name.clone(),
|
|
188
|
+
};
|
|
240
|
-
if
|
|
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(
|
|
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() !=
|
|
198
|
+
if bindings.len() != refs.len() {
|
|
253
|
-
return
|
|
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> =
|
|
204
|
+
let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
|
|
256
|
-
let mangled = mangle(&
|
|
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::
|
|
208
|
+
self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
|
|
265
209
|
}
|
|
266
|
-
call.name =
|
|
210
|
+
call.name = mangled;
|
|
267
211
|
Ok(())
|
|
268
212
|
}
|
|
269
213
|
```
|
|
270
214
|
|
|
271
|
-
**
|
|
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
|
-
|
|
222
|
+
and change to:
|
|
274
223
|
|
|
275
224
|
```rust
|
|
276
|
-
ast::Stmt::Match(m) => {
|
|
277
|
-
|
|
225
|
+
self.resolve_enum_instantiation(call, env)?;
|
|
278
|
-
|
|
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
|
-
|
|
227
|
+
self.resolve_bare_generic_fn_instantiation(call, env)?;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
228
|
```
|
|
294
229
|
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
Replace:
|
|
239
|
+
and change to:
|
|
347
240
|
|
|
348
241
|
```rust
|
|
349
|
-
|
|
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
|
-
|
|
243
|
+
is_generic_param_name(&rt.name)
|
|
353
|
-
|
|
244
|
+
|| self.classes_generic.contains_key(&rt.name)
|
|
354
|
-
|
|
245
|
+
|| self.enums_generic_by_name.contains_key(&rt.name)
|
|
246
|
+
}
|
|
355
247
|
```
|
|
356
248
|
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
+
m.enums_generic_by_variant.insert(v.name.clone(), e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
_ => {}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
368
263
|
```
|
|
369
264
|
|
|
370
|
-
|
|
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
|
-
|
|
274
|
+
m.enums_generic_by_variant.insert(v.name.clone(), e);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
_ => {}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
376
280
|
```
|
|
377
281
|
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
+
Add a third `else if` branch:
|
|
385
302
|
|
|
386
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
let
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
336
|
+
and change to:
|
|
417
337
|
|
|
418
338
|
```rust
|
|
419
|
-
|
|
339
|
+
ast::Item::Fn(f) => {
|
|
420
|
-
|
|
340
|
+
let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
|
|
421
|
-
let
|
|
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
|
-
|
|
346
|
+
m.produced.push(ast::Item::Fn(f2));
|
|
347
|
+
}
|
|
423
348
|
}
|
|
424
349
|
```
|
|
425
350
|
|
|
426
|
-
|
|
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
|
|
367
|
+
- [ ] **Step 4: Run all the tests**
|
|
429
368
|
|
|
430
|
-
Run: `cargo test -p plum-checker --test checker_tests
|
|
431
|
-
Expected:
|
|
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
|
|
434
|
-
Expected:
|
|
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
|
|
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"
|