plum

#treesitter#compiler#wasm

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

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


docs/superpowers/plans/2026-07-20-generic-enum-multi-instantiation.md
aedebc1 1
# Generic Enum Multi-Instantiation Implementation Plan
aedebc1 2
aedebc1 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.
aedebc1 4
aedebc1 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.
aedebc1 6
aedebc1 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.
aedebc1 8
e698d53 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.
e698d53 10
aedebc1 11
**Tech Stack:** Rust (`plum-checker` crate only for this plan — `plum-core`/`plum-wasm-codegen` need no changes).
aedebc1 12
aedebc1 13
## Global Constraints
aedebc1 14
aedebc1 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.
e698d53 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.
e698d53 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).
e698d53 18
- Everything else the generics-monomorphization plan already scoped out (trait-bound enforcement; `libs/std` compiling as-is) is unchanged.
aedebc1 19
- Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use the `"monomorphize: ..."` prefix.
aedebc1 20
- Must leave `cargo test --workspace` and `npx --yes tree-sitter-cli test` (from `tooling/tree-sitter-plum/`) green.
aedebc1 21
aedebc1 22
---
aedebc1 23
e698d53 24
### Task 1: Specialize ordinary functions with a bare generic-class/enum-typed parameter
aedebc1 25
aedebc1 26
**Files:**
aedebc1 27
- Modify: `plum-checker/src/monomorphize.rs`
aedebc1 28
- Modify: `plum-checker/tests/checker_tests.rs`
aedebc1 29
- Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
e698d53 30
e698d53 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.
aedebc1 32
aedebc1 33
**Interfaces:**
e698d53 34
- Consumes: `enum_generic_params`, `class_generic_params`, `mangle`, `Substitution`, `specialize_fn` (all already exist, unchanged).
e698d53 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.
aedebc1 36
aedebc1 37
- [ ] **Step 1: Write failing tests**
aedebc1 38
e698d53 39
Append to `plum-checker/tests/checker_tests.rs`:
aedebc1 40
aedebc1 41
```rust
aedebc1 42
#[test]
e698d53 43
fn ordinary_function_with_bare_generic_enum_param_type_checks() {
e698d53 44
    // The shape that broke the pre-existing codegen test: an otherwise-ordinary
e698d53 45
    // function taking a bare generic-enum-typed parameter.
aedebc1 46
    let src = "\
aedebc1 47
enum Option =
aedebc1 48
  | Some(a)
aedebc1 49
  | None
aedebc1 50
e698d53 51
unwrapOr(o: Option, default: Int) -> Int =
aedebc1 52
  match o
aedebc1 53
    Some(v) =>
aedebc1 54
      v
aedebc1 55
    None =>
e698d53 56
      default
aedebc1 57
e698d53 58
use() -> Int =
e698d53 59
  unwrapOr(Some(5), 0)
aedebc1 60
";
aedebc1 61
    let source = parse(src);
aedebc1 62
    let result = check_source(&source);
aedebc1 63
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
aedebc1 64
e698d53 65
    // Directly prove `unwrapOr` itself got specialized (not left bare/unresolved).
aedebc1 66
    let mono = plum_checker::monomorphize::monomorphize_source(&source)
aedebc1 67
        .expect("monomorphize should succeed");
e698d53 68
    let has_specialized_unwrap_or = mono.items.iter().any(|it| matches!(it, Item::Fn(f)
e698d53 69
        if f.name.starts_with("unwrapOr$") && f.type_param.is_none()));
e698d53 70
    assert!(has_specialized_unwrap_or, "expected a specialized `unwrapOr$...` function in the output");
e698d53 71
}
e698d53 72
e698d53 73
#[test]
e698d53 74
fn ordinary_function_with_bare_generic_class_param_type_checks() {
e698d53 75
    // The same shape, for a generic CLASS param instead of an enum — untested until
e698d53 76
    // now, but the identical root cause: `Box` is dropped from the monomorphized
e698d53 77
    // output, so a bare `Box`-typed param would otherwise reference nothing.
e698d53 78
    let src = "\
e698d53 79
type Box(a) =
e698d53 80
  value: a
e698d53 81
e698d53 82
getBoxValue<Box>() -> a =
e698d53 83
  self.value
e698d53 84
e698d53 85
sumBox(b: Box) -> Int =
e698d53 86
  b.getBoxValue()
e698d53 87
e698d53 88
use() -> Int =
e698d53 89
  sumBox(Box(value: 5))
e698d53 90
";
e698d53 91
    let source = parse(src);
e698d53 92
    let result = check_source(&source);
e698d53 93
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
aedebc1 94
}
aedebc1 95
```
aedebc1 96
e698d53 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:
aedebc1 98
aedebc1 99
```rust
aedebc1 100
#[test]
e698d53 101
fn ordinary_function_with_bare_generic_class_param_runs_correctly() {
aedebc1 102
    let src = "\
e698d53 103
type Box(a) =
e698d53 104
  value: a
aedebc1 105
e698d53 106
getBoxValue<Box>() -> Int =
e698d53 107
  self.value
aedebc1 108
e698d53 109
sumBox(b: Box) -> Int =
e698d53 110
  b.getBoxValue()
aedebc1 111
aedebc1 112
main() -> Int =
e698d53 113
  sumBox(Box(value: 11))
aedebc1 114
";
aedebc1 115
    let source = parse(src);
aedebc1 116
    let bytes = compile_source(&source).expect("compile failed");
e698d53 117
    assert_eq!(run_main(&bytes), 11);
aedebc1 118
}
aedebc1 119
```
aedebc1 120
aedebc1 121
- [ ] **Step 2: Run to see them fail**
aedebc1 122
e698d53 123
Run: `cargo test -p plum-checker --test checker_tests ordinary_function_with_bare`
e698d53 124
Expected: both fail — `check_source` returns an error (the bare `Option`/`Box` param never resolves).
aedebc1 125
e698d53 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`
e698d53 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).
aedebc1 128
e698d53 129
- [ ] **Step 3: Read the current file, then make the edits**
aedebc1 130
e698d53 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.
aedebc1 132
e698d53 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:
aedebc1 134
aedebc1 135
```rust
e698d53 136
    /// The enum's own bare name -> the generic `Enum` — used to detect a bare
e698d53 137
    /// generic-enum-typed function param (e.g. `o: Option`), distinct from
e698d53 138
    /// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
e698d53 139
    /// sites like `Some(5)`).
e698d53 140
    enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
e698d53 141
    /// Free functions that are NOT generic by `fn_generic_params`'s lowercase-letter
e698d53 142
    /// convention, but whose param type(s) bare-name a generic class or enum (e.g.
e698d53 143
    /// `unwrapOr(o: Option, ...)`) — such a function still needs its own
e698d53 144
    /// per-call-site specialization, since its receiver generic class/enum is
e698d53 145
    /// dropped from the monomorphized output and the bare name would otherwise
e698d53 146
    /// resolve to nothing.
e698d53 147
    fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
aedebc1 148
```
aedebc1 149
e698d53 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:
aedebc1 151
aedebc1 152
```rust
e698d53 153
    /// The bare names of any generic class or enum referenced directly (not via a
e698d53 154
    /// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Option"` for
e698d53 155
    /// `unwrapOr(o: Option, default: Int) -> Int`. See `fns_bare_generic`'s doc
e698d53 156
    /// comment for why such a function needs its own specialization.
e698d53 157
    fn fn_bare_generic_refs(&self, f: &ast::Fn) -> Vec<String> {
e698d53 158
        let mut names: Vec<String> = Vec::new();
e698d53 159
        for p in &f.params {
e698d53 160
            let n = match &p.ty {
e698d53 161
                ast::ParamType::Type(t) => &t.name,
e698d53 162
                ast::ParamType::Variadic(t) => &t.name,
e698d53 163
            };
e698d53 164
            if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
e698d53 165
                && !names.iter().any(|x| x == n)
e698d53 166
            {
e698d53 167
                names.push(n.clone());
e698d53 168
            }
e698d53 169
        }
e698d53 170
        names
aedebc1 171
    }
aedebc1 172
e698d53 173
    /// Resolves a call to an otherwise-ordinary function whose param type(s)
e698d53 174
    /// bare-name a generic class/enum, specializing it per call site exactly like a
e698d53 175
    /// truly-generic function — reusing the same `PendingSpecialization::Fn`
e698d53 176
    /// worklist entry and the unmodified `specialize_fn`, whose substitution
e698d53 177
    /// mechanism already replaces any type whose bare name matches a substitution
e698d53 178
    /// key (it doesn't care whether that key came from a lowercase-letter generic
e698d53 179
    /// parameter or a bare generic class/enum reference).
e698d53 180
    fn resolve_bare_generic_fn_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
e698d53 181
        let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
e698d53 182
        let refs = self.fn_bare_generic_refs(f);
aedebc1 183
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
e698d53 184
        for (param, arg) in f.params.iter().zip(call.args.iter()) {
e698d53 185
            let n = match &param.ty {
e698d53 186
                ast::ParamType::Type(t) => t.name.clone(),
e698d53 187
                ast::ParamType::Variadic(t) => t.name.clone(),
e698d53 188
            };
e698d53 189
            if refs.contains(&n) {
aedebc1 190
                let arg_expr = match arg {
aedebc1 191
                    ast::Arg::Positional(e) => e,
aedebc1 192
                    ast::Arg::Keyword { value, .. } => value,
aedebc1 193
                    ast::Arg::Pair { value, .. } => value,
aedebc1 194
                };
e698d53 195
                bindings.entry(n).or_insert_with(|| self.infer(arg_expr, env));
aedebc1 196
            }
aedebc1 197
        }
e698d53 198
        if bindings.len() != refs.len() {
e698d53 199
            return Err(format!(
e698d53 200
                "monomorphize: could not resolve all generic parameters for '{}' at this call site",
e698d53 201
                call.name
e698d53 202
            ));
aedebc1 203
        }
e698d53 204
        let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
e698d53 205
        let mangled = mangle(&call.name, &type_args);
aedebc1 206
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
aedebc1 207
            self.enqueued.insert(mangled.clone());
e698d53 208
            self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
aedebc1 209
        }
e698d53 210
        call.name = mangled;
aedebc1 211
        Ok(())
aedebc1 212
    }
aedebc1 213
```
aedebc1 214
e698d53 215
**3c. Wire the new resolution into `rewrite_expr`'s `FnCall` arm.** Find:
aedebc1 216
e698d53 217
```rust
e698d53 218
                self.resolve_enum_instantiation(call, env)?;
e698d53 219
                self.resolve_fn_instantiation(call, env)?;
e698d53 220
```
e698d53 221
e698d53 222
and change to:
aedebc1 223
aedebc1 224
```rust
e698d53 225
                self.resolve_enum_instantiation(call, env)?;
e698d53 226
                self.resolve_fn_instantiation(call, env)?;
e698d53 227
                self.resolve_bare_generic_fn_instantiation(call, env)?;
aedebc1 228
```
aedebc1 229
e698d53 230
**3d. Extend `maybe_rewrite_return` to also recognize a bare generic *enum* return** (it already recognizes a bare generic *class* return). Find:
aedebc1 231
aedebc1 232
```rust
e698d53 233
            Some(rt) => {
e698d53 234
                is_generic_param_name(&rt.name)
e698d53 235
                    || self.classes_generic.contains_key(&rt.name)
aedebc1 236
            }
aedebc1 237
```
aedebc1 238
e698d53 239
and change to:
aedebc1 240
aedebc1 241
```rust
e698d53 242
            Some(rt) => {
e698d53 243
                is_generic_param_name(&rt.name)
e698d53 244
                    || self.classes_generic.contains_key(&rt.name)
e698d53 245
                    || self.enums_generic_by_name.contains_key(&rt.name)
e698d53 246
            }
aedebc1 247
```
aedebc1 248
e698d53 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`):
aedebc1 250
aedebc1 251
```rust
e698d53 252
    for item in &source.items {
e698d53 253
        match item {
e698d53 254
            ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
e698d53 255
            ast::Item::Enum(e) if !enum_generic_params(e).is_empty() => {
e698d53 256
                for v in &e.variants {
e698d53 257
                    m.enums_generic_by_variant.insert(v.name.clone(), e);
e698d53 258
                }
e698d53 259
            }
e698d53 260
            _ => {}
e698d53 261
        }
e698d53 262
    }
aedebc1 263
```
aedebc1 264
e698d53 265
Change the `Enum` arm to also populate `enums_generic_by_name`:
aedebc1 266
aedebc1 267
```rust
e698d53 268
    for item in &source.items {
e698d53 269
        match item {
e698d53 270
            ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
e698d53 271
            ast::Item::Enum(e) if !enum_generic_params(e).is_empty() => {
e698d53 272
                m.enums_generic_by_name.insert(e.name.clone(), e);
e698d53 273
                for v in &e.variants {
e698d53 274
                    m.enums_generic_by_variant.insert(v.name.clone(), e);
e698d53 275
                }
e698d53 276
            }
e698d53 277
            _ => {}
e698d53 278
        }
e698d53 279
    }
aedebc1 280
```
aedebc1 281
e698d53 282
Find the second loop (classifies `Fn` items into `methods_generic_on`/`fns_generic`):
aedebc1 283
aedebc1 284
```rust
e698d53 285
    for item in &source.items {
e698d53 286
        if let ast::Item::Fn(f) = item {
e698d53 287
            let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
e698d53 288
            if receiver_is_generic {
e698d53 289
                m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
e698d53 290
            } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
e698d53 291
                m.fns_generic.insert(f.name.clone(), f);
e698d53 292
            }
e698d53 293
            // A method whose receiver is NOT generic is left as a regular method below,
e698d53 294
            // even if its own params/return happen to use a bare lowercase-letter type
e698d53 295
            // name — that shape (a method introducing its own extra generic parameter)
e698d53 296
            // is out of scope for this pass; see the plan's Global Constraints.
e698d53 297
        }
e698d53 298
    }
aedebc1 299
```
aedebc1 300
e698d53 301
Add a third `else if` branch:
aedebc1 302
e698d53 303
```rust
e698d53 304
    for item in &source.items {
e698d53 305
        if let ast::Item::Fn(f) = item {
e698d53 306
            let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
e698d53 307
            if receiver_is_generic {
e698d53 308
                m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
e698d53 309
            } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
e698d53 310
                m.fns_generic.insert(f.name.clone(), f);
e698d53 311
            } else if f.type_param.is_none() && !m.fn_bare_generic_refs(f).is_empty() {
e698d53 312
                m.fns_bare_generic.insert(f.name.clone(), f);
e698d53 313
            }
e698d53 314
            // A method whose receiver is NOT generic is left as a regular method below,
e698d53 315
            // even if its own params/return happen to use a bare lowercase-letter type
e698d53 316
            // name, or bare-name a generic class/enum — those shapes are out of scope
e698d53 317
            // for this pass; see the plan's Global Constraints.
e698d53 318
        }
e698d53 319
    }
e698d53 320
```
e698d53 321
e698d53 322
**3f. Exclude `fns_bare_generic` members from direct pass-through.** Find the third loop's `Fn` arm:
aedebc1 323
aedebc1 324
```rust
e698d53 325
            ast::Item::Fn(f) => {
e698d53 326
                let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
e698d53 327
                let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
e698d53 328
                if !receiver_is_generic && !is_generic_fn {
e698d53 329
                    let mut f2 = f.clone();
e698d53 330
                    m.rewrite_fn_body(&mut f2, false)?;
e698d53 331
                    m.produced.push(ast::Item::Fn(f2));
aedebc1 332
                }
aedebc1 333
            }
aedebc1 334
```
aedebc1 335
e698d53 336
and change to:
aedebc1 337
aedebc1 338
```rust
e698d53 339
            ast::Item::Fn(f) => {
e698d53 340
                let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
e698d53 341
                let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
e698d53 342
                let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
e698d53 343
                if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
e698d53 344
                    let mut f2 = f.clone();
e698d53 345
                    m.rewrite_fn_body(&mut f2, false)?;
e698d53 346
                    m.produced.push(ast::Item::Fn(f2));
e698d53 347
                }
aedebc1 348
            }
aedebc1 349
```
aedebc1 350
e698d53 351
**3g. Initialize the two new fields** in the `Monomorphizer` struct literal. Find:
e698d53 352
e698d53 353
```rust
e698d53 354
        enums_generic_by_variant: BTreeMap::new(),
e698d53 355
        enum_variant_mangling: BTreeMap::new(),
e698d53 356
```
e698d53 357
e698d53 358
and change to:
aedebc1 359
e698d53 360
```rust
e698d53 361
        enums_generic_by_variant: BTreeMap::new(),
e698d53 362
        enums_generic_by_name: BTreeMap::new(),
e698d53 363
        enum_variant_mangling: BTreeMap::new(),
e698d53 364
        fns_bare_generic: BTreeMap::new(),
e698d53 365
```
e698d53 366
e698d53 367
- [ ] **Step 4: Run all the tests**
aedebc1 368
e698d53 369
Run: `cargo test -p plum-checker --test checker_tests`
e698d53 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.
aedebc1 371
e698d53 372
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
e698d53 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`.
aedebc1 374
e698d53 375
- [ ] **Step 5: Update README (this was deferred by the blocked prior attempt)**
aedebc1 376
aedebc1 377
In `README.md`, replace the sentence (currently around line 243):
aedebc1 378
aedebc1 379
```markdown
aedebc1 380
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).
aedebc1 381
```
aedebc1 382
aedebc1 383
with:
aedebc1 384
aedebc1 385
```markdown
aedebc1 386
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.
aedebc1 387
```
aedebc1 388
aedebc1 389
- [ ] **Step 6: Run the full workspace and tree-sitter suites**
aedebc1 390
aedebc1 391
```bash
aedebc1 392
cargo test --workspace
aedebc1 393
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
aedebc1 394
```
aedebc1 395
e698d53 396
Expected: fully green (aside from the pre-existing, intentionally-`#[ignore]`d slow recursion-guard test, unaffected by this change).
aedebc1 397
aedebc1 398
- [ ] **Step 7: Commit**
aedebc1 399
e698d53 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.
e698d53 401
aedebc1 402
```bash
aedebc1 403
git add plum-checker/src/monomorphize.rs plum-checker/tests/checker_tests.rs plum-wasm-codegen/tests/codegen_tests.rs README.md
aedebc1 404
git commit -m "feat(plum-checker): support multiple concrete instantiations of the same generic enum"
aedebc1 405
```