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-24-enum-discriminant-values.md
cb48db9 1
# Enum Discriminant Values Implementation Plan
cb48db9 2
cb48db9 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.
cb48db9 4
cb48db9 5
**Goal:** Let an enum declare shared, uniformly-typed value fields (`enum Step(n: Int) = | READ_MIN_OCCURANCES(0) | ...`), readable via ordinary field access (`self.n`) on any instance regardless of variant, per `docs/superpowers/specs/2026-07-24-enum-discriminant-values-design.md`.
cb48db9 6
cb48db9 7
**Architecture:** Grammar (new `enum_param` rule + paren-form variant values, alongside today's unchanged bracket-form generic payload) → AST (`Enum.params`, `EnumVariant.values`) → parser → checker (arity/type validation against declared params; discriminant enums also get an entry in `ClassEnv`-shaped bookkeeping so field access can reuse the SAME offset-load mechanism a class field already uses, adjusted by a constant +1 slot for the leading tag — not literally inserted into `ClassEnv` itself, since that would misalign offsets by one slot; a small dedicated lookup path instead) → codegen (bare variant references belonging to a discriminant enum reuse the EXISTING payload-variant construction path via a synthetically-built `ast::FnCall` carrying the variant's declared literal values as its "arguments" — no new heap-layout code needed at all, since `[tag][field0][field1]...` is already exactly what payload variants use).
cb48db9 8
cb48db9 9
**Tech Stack:** tree-sitter (JS grammar), Rust (`plum-core`, `plum-checker`, `plum-wasm-codegen`).
cb48db9 10
cb48db9 11
## Global Constraints
cb48db9 12
cb48db9 13
- Spec: `docs/superpowers/specs/2026-07-24-enum-discriminant-values-design.md`
cb48db9 14
- A variant has either non-empty `fields` (today's generic-payload feature, bracket-delimited `[...]`) or non-empty `values` (this feature, paren-delimited `(...)`), never both — checker-enforced, not grammar-enforced.
cb48db9 15
- An enum with declared `params` requires every variant to supply exactly `params.len()` values, type-unified against the params' declared types.
cb48db9 16
- Field access (`self.n`) on a discriminant-enum value must load from offset `(param_idx + 1) * 8` — **not** `param_idx * 8` — because slot 0 is always the tag, exactly matching today's payload-variant heap layout (`[tag: i32][field0][field1]...`). Getting this offset wrong is the single highest-risk mistake in this plan; Task 3's steps call this out explicitly.
cb48db9 17
- Ordinary enums (`Option`, `Result`, `Bool`, `Color`, ...) are completely unaffected — this is purely additive, gated on `params`/`values` being non-empty.
cb48db9 18
- Run `cargo test --workspace` and (from `tooling/tree-sitter-plum/`) `npx --yes tree-sitter-cli test` after every task.
cb48db9 19
cb48db9 20
---
cb48db9 21
cb48db9 22
### Task 1: Grammar — enum params and paren-form variant values
cb48db9 23
cb48db9 24
**Files:**
cb48db9 25
- Modify: `tooling/tree-sitter-plum/grammar.js`
cb48db9 26
- Modify: `tooling/tree-sitter-plum/test/corpus/enum.txt`
cb48db9 27
cb48db9 28
**Interfaces:**
cb48db9 29
- Consumes: nothing.
cb48db9 30
- Produces: `enum` gains an optional `field("params", ...)` before `"="`; `enum_field`'s existing `parameters` field gains a second alternative — parens containing an expression list (discriminant values) alongside the existing bracket-delimited type list.
cb48db9 31
cb48db9 32
- [ ] **Step 1: Read the current `enum`/`enum_field` rules first**
cb48db9 33
cb48db9 34
Read `tooling/tree-sitter-plum/grammar.js`'s current `enum` and `enum_field` rules directly — this plan's earlier research excerpts may have drifted from the exact current text (e.g. after Task 1 of the bracket-generics migration, `enum_field`'s parameters use `"[" commaSep1(choice($.type_identifier, $.generic)) "]"`). Use the actual current text as your starting point for the edits below, not this plan's paraphrase.
cb48db9 35
cb48db9 36
- [ ] **Step 2: Add `enum_param` and the `params` field to `enum`**
cb48db9 37
cb48db9 38
Add a new rule (near `generic_type`/`param`):
cb48db9 39
cb48db9 40
```js
cb48db9 41
    enum_param: ($) =>
cb48db9 42
      seq(field("name", $.var_identifier), ":", field("type", $.type)),
cb48db9 43
```
cb48db9 44
cb48db9 45
Extend `enum`'s rule to add an optional `params` field between the name and `"="` (mirroring how `class`'s `generics` field is positioned):
cb48db9 46
cb48db9 47
```js
cb48db9 48
    enum: ($) =>
cb48db9 49
      seq(
cb48db9 50
        "enum",
cb48db9 51
        field("name", $.type_identifier),
cb48db9 52
        field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
cb48db9 53
        "=",
cb48db9 54
        $._indent,
cb48db9 55
        optional(repeat(alias($.enum_field, $.field))),
cb48db9 56
        $._dedent,
cb48db9 57
      ),
cb48db9 58
```
cb48db9 59
cb48db9 60
- [ ] **Step 3: Add the paren-form alternative to `enum_field`'s parameters**
cb48db9 61
cb48db9 62
Extend the existing bracket-only choice to also allow a paren-delimited expression list:
cb48db9 63
cb48db9 64
```js
cb48db9 65
    enum_field: ($) =>
cb48db9 66
      seq(
cb48db9 67
        "|",
cb48db9 68
        field("name", $.type_identifier),
cb48db9 69
        field(
cb48db9 70
          "parameters",
cb48db9 71
          optional(choice(
cb48db9 72
            seq("[", commaSep1(choice($.type_identifier, $.generic)), "]"), // existing: generic type payload
cb48db9 73
            seq("(", commaSep1($.expression), ")"),                        // new: discriminant value literals
cb48db9 74
          )),
cb48db9 75
        ),
cb48db9 76
      ),
cb48db9 77
```
cb48db9 78
cb48db9 79
- [ ] **Step 4: Regenerate the parser**
cb48db9 80
cb48db9 81
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate`
cb48db9 82
Expected: succeeds with no conflicts. If tree-sitter reports one (e.g. between the two `enum_field` parameter alternatives — an integer literal vs. a `type_identifier` can both start with different tokens, so this SHOULD be conflict-free, but verify), stop and report BLOCKED.
cb48db9 83
cb48db9 84
- [ ] **Step 5: Add corpus tests**
cb48db9 85
cb48db9 86
In `tooling/tree-sitter-plum/test/corpus/enum.txt`, add a new test block (match the file's existing header/divider format exactly — read it first):
cb48db9 87
cb48db9 88
```
cb48db9 89
================================================================================
cb48db9 90
enum - discriminant values
cb48db9 91
================================================================================
cb48db9 92
cb48db9 93
enum Step(n: Int) =
cb48db9 94
  | READ_MIN_OCCURANCES(0)
cb48db9 95
  | READ_MAX_OCCURANCES(1)
cb48db9 96
```
cb48db9 97
cb48db9 98
Do not hand-guess the expected S-expression tree — run Step 6 first, capture the REAL parser output, and paste that into the corpus file (same practice as the bracket-generics-migration and guard-clause-match plans' corpus steps).
cb48db9 99
cb48db9 100
- [ ] **Step 6: Run the corpus tests, fix the expected tree from real output, iterate to green**
cb48db9 101
cb48db9 102
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test 2>&1 | tail -100`, using `npx --yes tree-sitter-cli parse -` on the new source (or a temp file) to get ground truth for the expected tree. Iterate until 100% pass including the new test AND every pre-existing test (in particular, re-confirm the EXISTING "enum - generic variant fields" test, `| Some[T]`, still passes unchanged — this task must not regress the bracket-form path).
cb48db9 103
cb48db9 104
- [ ] **Step 7: Commit**
cb48db9 105
cb48db9 106
```bash
cb48db9 107
git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/src tooling/tree-sitter-plum/test/corpus/enum.txt
cb48db9 108
git commit -m "feat(grammar): add enum discriminant-value declarations"
cb48db9 109
```
cb48db9 110
cb48db9 111
---
cb48db9 112
cb48db9 113
### Task 2: `plum-core` — AST and parser support
cb48db9 114
cb48db9 115
**Files:**
cb48db9 116
- Modify: `plum-core/src/ast.rs`
cb48db9 117
- Modify: `plum-core/src/parser.rs`
cb48db9 118
- Modify: `plum-core/tests/parser_test.rs`
cb48db9 119
cb48db9 120
**Interfaces:**
cb48db9 121
- Consumes: the regenerated grammar from Task 1 (`enum` node with an optional `"params"`-field child list of `enum_param` nodes; `enum_field` node whose `"parameters"` field may now contain either `type_identifier`/`generic` children (existing) OR arbitrary expression children (new)).
cb48db9 122
- Produces: `Enum.params: Vec<EnumParam>` (new, empty for ordinary enums); `EnumVariant.values: Vec<Expr>` (new, empty for ordinary variants).
cb48db9 123
cb48db9 124
- [ ] **Step 1: Add `EnumParam` and extend `Enum`/`EnumVariant` in `ast.rs`**
cb48db9 125
cb48db9 126
Read the current `Enum`/`EnumVariant` structs first (they may have shifted slightly). Add:
cb48db9 127
cb48db9 128
```rust
cb48db9 129
#[derive(Debug, Clone, PartialEq)]
cb48db9 130
pub struct EnumParam {
cb48db9 131
    pub name: String,
cb48db9 132
    pub ty: Type,
cb48db9 133
}
cb48db9 134
```
cb48db9 135
cb48db9 136
Extend `Enum`:
cb48db9 137
cb48db9 138
```rust
cb48db9 139
#[derive(Debug, Clone, PartialEq)]
cb48db9 140
pub struct Enum {
cb48db9 141
    pub name: String,
cb48db9 142
    pub params: Vec<EnumParam>,   // new; empty for an enum declared without "(...)"
cb48db9 143
    pub variants: Vec<EnumVariant>,
cb48db9 144
}
cb48db9 145
```
cb48db9 146
cb48db9 147
Extend `EnumVariant`:
cb48db9 148
cb48db9 149
```rust
cb48db9 150
#[derive(Debug, Clone, PartialEq)]
cb48db9 151
pub struct EnumVariant {
cb48db9 152
    pub name: String,
cb48db9 153
    pub fields: Vec<String>,  // existing, unchanged meaning
cb48db9 154
    pub values: Vec<Expr>,    // new; empty for an ordinary/generic variant
cb48db9 155
}
cb48db9 156
```
cb48db9 157
cb48db9 158
Every existing call site that constructs an `Enum`/`EnumVariant` literal (in `plum-core/src/parser.rs`, and any test fixture that builds one directly rather than through parsing) needs its `params: vec![]` / `values: vec![]` added — grep for `Enum {` and `EnumVariant {` across the workspace and fix each one.
cb48db9 159
cb48db9 160
- [ ] **Step 2: Parse `enum`'s new `params` field and `enum_field`'s value-list alternative**
cb48db9 161
cb48db9 162
Read `parse_enum`/`parse_enum_variant` in `plum-core/src/parser.rs` (their exact current bodies — this plan's earlier research summarized them but may be slightly stale). Extend `parse_enum` to also collect `enum_param` children into `Enum.params` (a small new helper `parse_enum_param(&self, node: Node) -> EnumParam` mirroring `parse_field`'s two-child-read shape: name then type). Extend `parse_enum_variant` so that when a `parameters`-field child is an expression node (not `type_identifier`/`generic`), it's parsed via `parse_expression`/`unwrap_expr_node` into `EnumVariant.values` instead of `EnumVariant.fields` — the two are populated from disjoint child-kind sets (type-identifier/generic-kind children go to `fields` as today; everything else under the same field position is an expression, goes to `values`).
cb48db9 163
cb48db9 164
- [ ] **Step 3: Add a parser test**
cb48db9 165
cb48db9 166
In `plum-core/tests/parser_test.rs`, parse the `Step` example from Task 1 and assert: `Enum.params == vec![EnumParam { name: "n", ty: Type { name: "Int", generics: vec![] } }]`, and each variant's `values` is `vec![Expr::Int(0)]` / `vec![Expr::Int(1)]` respectively, with `fields` empty for both.
cb48db9 167
cb48db9 168
- [ ] **Step 4: Run `plum-core`'s tests**
cb48db9 169
cb48db9 170
Run: `cargo test -p plum-core 2>&1 | tail -60`
cb48db9 171
Expected: PASS.
cb48db9 172
cb48db9 173
- [ ] **Step 5: Commit**
cb48db9 174
cb48db9 175
```bash
cb48db9 176
git add plum-core/src/ast.rs plum-core/src/parser.rs plum-core/tests/parser_test.rs
cb48db9 177
git commit -m "feat(plum-core): parse enum discriminant-value declarations"
cb48db9 178
```
cb48db9 179
cb48db9 180
---
cb48db9 181
cb48db9 182
### Task 3: `plum-checker` — validate discriminant values and enable field access
cb48db9 183
cb48db9 184
**Files:**
cb48db9 185
- Modify: `plum-checker/src/lib.rs`
cb48db9 186
- Modify: `plum-checker/tests/checker_tests.rs`
cb48db9 187
cb48db9 188
**Interfaces:**
cb48db9 189
- Consumes: `Enum.params`/`EnumVariant.values` from Task 2.
cb48db9 190
- Produces: `EnumVariantInfo` gains a `values: Vec<ast::Expr>` field (populated only for discriminant variants, consumed by `plum-wasm-codegen` in Task 4); arity/type validation errors for malformed discriminant declarations; `self.n`-style field access resolves correctly on a discriminant-enum receiver.
cb48db9 191
cb48db9 192
This is the highest-risk task in this plan — the field-access offset (`(param_idx + 1) * 8`, not `param_idx * 8`) is easy to get wrong, since it's tempting to just reuse `ClassEnv`'s exact field-access code path (which assumes NO leading tag slot). **Do not** insert a discriminant enum's params directly into `ClassEnv` — that would misalign every field by one 8-byte slot (it would read/write the tag instead of the actual value). Read the "Field access" bullet in the design spec's Codegen section again before starting this task if anything below is unclear.
cb48db9 193
cb48db9 194
- [ ] **Step 1: Read `build_global_tables`, `CheckCtx`, `EnumVariantInfo` and the field-access check function first**
cb48db9 195
cb48db9 196
Read their current exact bodies in `plum-checker/src/lib.rs` (file:line references from the design survey, which may have drifted: `EnumVariantInfo`/`EnumVariants`/`ClassEnv`/`CheckCtx` type defs around lines 44-66; `build_global_tables` around lines 72-143; the field-access check inside whatever function checks `ast::AttrKind::Field` — search for `"cannot access field"` or similar).
cb48db9 197
cb48db9 198
- [ ] **Step 2: Extend `EnumVariantInfo` with a `values` field**
cb48db9 199
cb48db9 200
```rust
cb48db9 201
#[derive(Debug, Clone, PartialEq)]
cb48db9 202
pub struct EnumVariantInfo {
cb48db9 203
    pub enum_name: String,
cb48db9 204
    pub tag: i32,
cb48db9 205
    pub field_types: Vec<PlumType>,
cb48db9 206
    pub values: Vec<ast::Expr>,  // new; non-empty only for a discriminant variant
cb48db9 207
}
cb48db9 208
```
cb48db9 209
cb48db9 210
Every existing construction site of `EnumVariantInfo` (there are at least two in `build_global_tables` — one for the built-in `Bool` variants, one in the enum-registration loop) needs `values: vec![]` added for the ordinary case.
cb48db9 211
cb48db9 212
- [ ] **Step 3: Populate `field_types`/`values` correctly for discriminant enums in `build_global_tables`**
cb48db9 213
cb48db9 214
The current enum-registration loop (inside `build_global_tables`'s first pass) computes each variant's `field_types` from `v.fields` (type-name strings). For a discriminant enum (`e.params` non-empty), EVERY variant's `field_types` must instead come from `e.params` (the same list, shared across all variants of that enum — this is what makes `self.n` resolve to a consistent type regardless of variant), and `values` comes from that specific variant's own `v.values`. Something like:
cb48db9 215
cb48db9 216
```rust
cb48db9 217
ast::Item::Enum(e) => {
cb48db9 218
    let shared_field_types: Vec<PlumType> = e.params.iter()
cb48db9 219
        .map(|p| plum_type_from_ast(&p.ty))
cb48db9 220
        .collect();
cb48db9 221
    for (tag, v) in e.variants.iter().enumerate() {
cb48db9 222
        let field_types = if !e.params.is_empty() {
cb48db9 223
            shared_field_types.clone()
cb48db9 224
        } else {
cb48db9 225
            v.fields.iter()
cb48db9 226
                .map(|f| plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] }))
cb48db9 227
                .collect()
cb48db9 228
        };
cb48db9 229
        enum_variants.insert(v.name.clone(), EnumVariantInfo {
cb48db9 230
            enum_name: e.name.clone(),
cb48db9 231
            tag: tag as i32,
cb48db9 232
            field_types,
cb48db9 233
            values: v.values.clone(),
cb48db9 234
        });
cb48db9 235
    }
cb48db9 236
}
cb48db9 237
```
cb48db9 238
cb48db9 239
- [ ] **Step 4: Add arity/type validation for discriminant declarations**
cb48db9 240
cb48db9 241
Somewhere in `check_source` (or a small new helper called from it, once per `Item::Enum`), for every enum with non-empty `params`: every variant must have `values.len() == params.len()` (error otherwise, naming the variant and the expected/actual counts), and each value's inferred type (`infer_expr`) must unify against the corresponding param's declared type. For an enum with EMPTY `params`, every variant's `values` must also be empty — a non-empty `values` there is an error too (most likely caused by accidentally writing e.g. `Some(5)` where `Some[Int]` was meant). Add this validation pass; follow this file's existing conventions for where per-item declaration-level checks like this live (e.g. is there already a similar pass for classes/traits you can mirror the placement of).
cb48db9 242
cb48db9 243
- [ ] **Step 5: Enable field access on a discriminant enum receiver**
cb48db9 244
cb48db9 245
Add a new lookup table alongside `ClassEnv`/`EnumVariants` — reuse the exact same underlying shape (`Vec<(String, PlumType)>` per enum name) so the code reads almost identically to today's class-field lookup, but keep it a SEPARATE map (do not merge into `ClassEnv` — see the offset-misalignment warning above):
cb48db9 246
cb48db9 247
```rust
cb48db9 248
/// Field names and types for every discriminant enum's shared params (`enum Foo(n: Int) = ...`),
cb48db9 249
/// keyed by the ENUM's name (not a variant name) — e.g. `"Step" -> [("n", TInt)]`. Field
cb48db9 250
/// access on a value of this type must load/store at offset `(field_idx + 1) * 8`, NOT
cb48db9 251
/// `field_idx * 8` like a class — slot 0 is always the variant's tag.
cb48db9 252
pub type EnumParams = BTreeMap<String, Vec<(String, PlumType)>>;
cb48db9 253
```
cb48db9 254
cb48db9 255
Populate it in `build_global_tables` (one entry per discriminant enum, empty/no entry for ordinary enums), thread it through `CheckCtx` as a new field (`pub enum_params: &'a EnumParams`), and update `build_global_tables`'s return type/signature and its ONE call site in `check_source` (search for other callers too — `plum-wasm-codegen` calls this same function, see Task 4).
cb48db9 256
cb48db9 257
In the field-access check function, add a fallback: if `obj_ty`'s name isn't found in `ctx.classes`, check `ctx.enum_params` before falling through to today's permissive "unmodeled type" escape hatch — resolve the field the same way a class field would be (find by name, unify against its declared type), just sourced from `enum_params` instead of `classes`.
cb48db9 258
cb48db9 259
- [ ] **Step 6: Add checker tests**
cb48db9 260
cb48db9 261
In `plum-checker/tests/checker_tests.rs`:
cb48db9 262
1. The `Step` example type-checks with no errors.
cb48db9 263
2. A variant with the wrong number of discriminant values produces an error.
cb48db9 264
3. A variant with a wrongly-typed discriminant value (e.g. a `Str` literal where `Int` is declared) produces an error.
cb48db9 265
4. A non-empty `values` on a variant of a param-less (ordinary) enum produces an error.
cb48db9 266
5. `self.n` (or `obj.n`) field access inside a method on a discriminant-enum receiver type-checks and resolves to the declared param's type; the same on an ORDINARY enum (no `params`) still fails exactly as it does today.
cb48db9 267
cb48db9 268
- [ ] **Step 7: Run `plum-checker`'s tests**
cb48db9 269
cb48db9 270
Run: `cargo test -p plum-checker 2>&1 | tail -100`
cb48db9 271
Expected: PASS, including your new tests, and every pre-existing enum/field-access test.
cb48db9 272
cb48db9 273
- [ ] **Step 8: Commit**
cb48db9 274
cb48db9 275
```bash
cb48db9 276
git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs
cb48db9 277
git commit -m "feat(plum-checker): validate enum discriminant values, enable field access on them"
cb48db9 278
```
cb48db9 279
cb48db9 280
---
cb48db9 281
cb48db9 282
### Task 4: `plum-wasm-codegen` — compile discriminant-enum construction and field access
cb48db9 283
cb48db9 284
**Files:**
cb48db9 285
- Modify: `plum-wasm-codegen/src/lib.rs`
cb48db9 286
- Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
cb48db9 287
cb48db9 288
**Interfaces:**
cb48db9 289
- Consumes: `EnumVariantInfo.values`/`field_types` (Task 3), `EnumParams` (Task 3) threaded into `LocalCtx`.
cb48db9 290
- Produces: a bare discriminant-variant reference (`READ_MIN_OCCURANCES`) heap-allocates and stores `[tag][value0][value1]...` using the variant's OWN declared literal values (not call-site args, since there is no call site); `self.n` field access loads from `(field_idx + 1) * 8` off a discriminant-enum receiver.
cb48db9 291
cb48db9 292
- [ ] **Step 1: Read the current `compile_expr`'s `ast::Expr::TypeName` arm, `compile_variant_construction`, the `Attribute`/`Field` codegen arm, `build_global_tables`'s one call site, and the `Collector`'s `walk_expr` first**
cb48db9 293
cb48db9 294
These are all in `plum-wasm-codegen/src/lib.rs`; use file:line references from the design survey as a starting point but READ the actual current code — this is the largest, most detail-sensitive file touched by this plan.
cb48db9 295
cb48db9 296
- [ ] **Step 2: Thread `EnumParams` into `LocalCtx` and its one construction site**
cb48db9 297
cb48db9 298
Add `enum_params: &'a plum_checker::EnumParams` to the `LocalCtx` struct (alongside the existing `classes: &'a ClassEnv` field), and update `build_global_tables`'s call site (now returning an extra value) plus wherever `LocalCtx` is constructed (there appear to be two near-duplicate codegen entry points in this file, given the earlier survey found `match_scratch_base`/`match_scratch_index` set up twice, around lines ~1843 and ~3387 — both need the new field threaded through if both actually construct a full `LocalCtx`; verify this by reading, don't assume).
cb48db9 299
cb48db9 300
- [ ] **Step 3: Make a bare discriminant-variant reference construct correctly**
cb48db9 301
cb48db9 302
The current `ast::Expr::TypeName(n)` arm in `compile_expr` (search `ast::Expr::TypeName(n) => match ctx.enum_variants.get(n)`) special-cases `info.field_types.is_empty()` (bare tag) vs. non-empty (today: an error, "carries a payload — construct it with '...(...)'").  Add a third case: `field_types` non-empty AND `info.values` non-empty (a discriminant variant) — construct it via the EXISTING `compile_variant_construction` function, unchanged, by building a synthetic `ast::FnCall` whose `args` are `info.values` wrapped as `ast::Arg::Positional`:
cb48db9 303
cb48db9 304
```rust
cb48db9 305
ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) {
cb48db9 306
    Some(info) if info.field_types.is_empty() => {
cb48db9 307
        Instruction::I32Const(info.tag).encode(body);
cb48db9 308
    }
cb48db9 309
    Some(info) if !info.values.is_empty() => {
cb48db9 310
        let synthetic_call = ast::FnCall {
cb48db9 311
            name: n.clone(),
cb48db9 312
            args: info.values.iter().cloned().map(ast::Arg::Positional).collect(),
cb48db9 313
        };
cb48db9 314
        compile_variant_construction(info, &synthetic_call, expr, body, ctx, state)?;
cb48db9 315
    }
cb48db9 316
    Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
cb48db9 317
    None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
cb48db9 318
},
cb48db9 319
```
cb48db9 320
cb48db9 321
`compile_variant_construction` needs NO changes — it only reads `call.args`/`call.name` and `expr`'s pointer identity for scratch-slot lookup, all of which the synthetic call/the original `expr` already provide correctly.
cb48db9 322
cb48db9 323
- [ ] **Step 4: Register a scratch slot for bare discriminant-variant references in the `Collector`**
cb48db9 324
cb48db9 325
`compile_variant_construction`'s heap-allocation path requires a `classcall_scratch` entry keyed by the `expr` pointer (see `ctx.classcall_scratch.get(&scratch_key)`, which errors with "internal codegen error: missing variant-call scratch slot" if absent). Today, the `Collector`'s `walk_expr` only registers this slot for `ast::Expr::ClassCall` and payload-carrying `ast::Expr::FnCall` (search `classcall_scratch.insert` — there are two near-duplicate `Collector`-like structs in this file per the earlier survey; both need this fix, verify by reading). Add a new arm for `ast::Expr::TypeName(n)`:
cb48db9 326
cb48db9 327
```rust
cb48db9 328
ast::Expr::TypeName(n) => {
cb48db9 329
    let carries_baked_in_payload = self.cctx.enum_variants.get(n)
cb48db9 330
        .map(|info| !info.values.is_empty())
cb48db9 331
        .unwrap_or(false);
cb48db9 332
    if carries_baked_in_payload {
cb48db9 333
        let idx = self.next_classcall_slot;
cb48db9 334
        self.next_classcall_slot += 1;
cb48db9 335
        self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
cb48db9 336
    }
cb48db9 337
}
cb48db9 338
```
cb48db9 339
cb48db9 340
Match this file's existing style for where `ast::Expr::TypeName` currently falls in this match (it may currently be in a catch-all arm doing nothing — check, and add this specific arm before/instead of that catch-all as appropriate).
cb48db9 341
cb48db9 342
- [ ] **Step 5: Field access on a discriminant-enum receiver**
cb48db9 343
cb48db9 344
In `compile_expr`'s `ast::AttrKind::Field(field_name)` arm (search `"cannot access field"`), add a fallback: if `obj_ty`'s name isn't in `ctx.classes`, check `ctx.enum_params` before erroring — resolve `field_idx`/`field_ty` the same way the class path does, but compute `offset = ((field_idx + 1) as u64) * 8` (the `+1` is the critical difference — slot 0 is always the tag). Compile `attr.object` (pushes the heap pointer, exactly as the class path does) then load at that offset with the width matching `field_ty` (`I64Load`/`F64Load`/`I32Load`, same pattern as the class path).
cb48db9 345
cb48db9 346
- [ ] **Step 6: Add codegen tests**
cb48db9 347
cb48db9 348
In `plum-wasm-codegen/tests/codegen_tests.rs`:
cb48db9 349
1. A method reading `self.n` off a `Step`-shaped discriminant enum returns the correct declared value for at least two different variants (proving per-instance correctness, not a hardcoded constant) — compile and `run_main` an end-to-end program using the `Step` example.
cb48db9 350
2. A discriminant-enum value used in `match` (destructuring by variant NAME, not by its field) still resolves and runs correctly — since these variants are now always heap pointers (matching payload-variant layout), this should already work via the existing pattern-match codegen path unchanged; write a test proving it.
cb48db9 351
cb48db9 352
- [ ] **Step 7: Run the codegen tests**
cb48db9 353
cb48db9 354
Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -100`
cb48db9 355
Expected: PASS, including your new tests and every pre-existing enum/field-access/pattern-match test (in particular, the payload-free-variant and payload-variant tests noted in the design spec's survey — confirm neither regressed).
cb48db9 356
cb48db9 357
- [ ] **Step 8: Run the full workspace suite**
cb48db9 358
cb48db9 359
Run: `cargo test --workspace 2>&1 | tail -100`
cb48db9 360
Expected: all PASS.
cb48db9 361
cb48db9 362
- [ ] **Step 9: Commit**
cb48db9 363
cb48db9 364
```bash
cb48db9 365
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
cb48db9 366
git commit -m "feat(plum-wasm-codegen): compile enum discriminant-value construction and field access"
cb48db9 367
```
cb48db9 368
cb48db9 369
---
cb48db9 370
cb48db9 371
### Task 5: Final verification
cb48db9 372
cb48db9 373
**Files:** none (verification only).
cb48db9 374
cb48db9 375
- [ ] **Step 1: Full workspace test suite**
cb48db9 376
cb48db9 377
Run: `cargo test --workspace 2>&1 | tail -100`
cb48db9 378
Expected: all PASS.
cb48db9 379
cb48db9 380
- [ ] **Step 2: Tree-sitter corpus suite**
cb48db9 381
cb48db9 382
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test 2>&1 | tail -60`
cb48db9 383
Expected: all PASS.
cb48db9 384
cb48db9 385
- [ ] **Step 3: No commit needed** — verification only.