plum
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. |