plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
cb48db9
— Peter John
2026-07-24T12:30:18+05:30
docs: add implementation plan for enum discriminant values
docs/superpowers/plans/2026-07-24-enum-discriminant-values.md
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Enum Discriminant Values Implementation Plan
|
|
2
|
+
|
|
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.
|
|
4
|
+
|
|
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`.
|
|
6
|
+
|
|
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).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** tree-sitter (JS grammar), Rust (`plum-core`, `plum-checker`, `plum-wasm-codegen`).
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Spec: `docs/superpowers/specs/2026-07-24-enum-discriminant-values-design.md`
|
|
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.
|
|
15
|
+
- An enum with declared `params` requires every variant to supply exactly `params.len()` values, type-unified against the params' declared types.
|
|
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.
|
|
17
|
+
- Ordinary enums (`Option`, `Result`, `Bool`, `Color`, ...) are completely unaffected — this is purely additive, gated on `params`/`values` being non-empty.
|
|
18
|
+
- Run `cargo test --workspace` and (from `tooling/tree-sitter-plum/`) `npx --yes tree-sitter-cli test` after every task.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: Grammar — enum params and paren-form variant values
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Modify: `tooling/tree-sitter-plum/grammar.js`
|
|
26
|
+
- Modify: `tooling/tree-sitter-plum/test/corpus/enum.txt`
|
|
27
|
+
|
|
28
|
+
**Interfaces:**
|
|
29
|
+
- Consumes: nothing.
|
|
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.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Read the current `enum`/`enum_field` rules first**
|
|
33
|
+
|
|
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.
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 2: Add `enum_param` and the `params` field to `enum`**
|
|
37
|
+
|
|
38
|
+
Add a new rule (near `generic_type`/`param`):
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
enum_param: ($) =>
|
|
42
|
+
seq(field("name", $.var_identifier), ":", field("type", $.type)),
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Extend `enum`'s rule to add an optional `params` field between the name and `"="` (mirroring how `class`'s `generics` field is positioned):
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
enum: ($) =>
|
|
49
|
+
seq(
|
|
50
|
+
"enum",
|
|
51
|
+
field("name", $.type_identifier),
|
|
52
|
+
field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
|
|
53
|
+
"=",
|
|
54
|
+
$._indent,
|
|
55
|
+
optional(repeat(alias($.enum_field, $.field))),
|
|
56
|
+
$._dedent,
|
|
57
|
+
),
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- [ ] **Step 3: Add the paren-form alternative to `enum_field`'s parameters**
|
|
61
|
+
|
|
62
|
+
Extend the existing bracket-only choice to also allow a paren-delimited expression list:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
enum_field: ($) =>
|
|
66
|
+
seq(
|
|
67
|
+
"|",
|
|
68
|
+
field("name", $.type_identifier),
|
|
69
|
+
field(
|
|
70
|
+
"parameters",
|
|
71
|
+
optional(choice(
|
|
72
|
+
seq("[", commaSep1(choice($.type_identifier, $.generic)), "]"), // existing: generic type payload
|
|
73
|
+
seq("(", commaSep1($.expression), ")"), // new: discriminant value literals
|
|
74
|
+
)),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- [ ] **Step 4: Regenerate the parser**
|
|
80
|
+
|
|
81
|
+
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate`
|
|
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.
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 5: Add corpus tests**
|
|
85
|
+
|
|
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):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
================================================================================
|
|
90
|
+
enum - discriminant values
|
|
91
|
+
================================================================================
|
|
92
|
+
|
|
93
|
+
enum Step(n: Int) =
|
|
94
|
+
| READ_MIN_OCCURANCES(0)
|
|
95
|
+
| READ_MAX_OCCURANCES(1)
|
|
96
|
+
```
|
|
97
|
+
|
|
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).
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 6: Run the corpus tests, fix the expected tree from real output, iterate to green**
|
|
101
|
+
|
|
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).
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 7: Commit**
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/src tooling/tree-sitter-plum/test/corpus/enum.txt
|
|
108
|
+
git commit -m "feat(grammar): add enum discriminant-value declarations"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Task 2: `plum-core` — AST and parser support
|
|
114
|
+
|
|
115
|
+
**Files:**
|
|
116
|
+
- Modify: `plum-core/src/ast.rs`
|
|
117
|
+
- Modify: `plum-core/src/parser.rs`
|
|
118
|
+
- Modify: `plum-core/tests/parser_test.rs`
|
|
119
|
+
|
|
120
|
+
**Interfaces:**
|
|
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)).
|
|
122
|
+
- Produces: `Enum.params: Vec<EnumParam>` (new, empty for ordinary enums); `EnumVariant.values: Vec<Expr>` (new, empty for ordinary variants).
|
|
123
|
+
|
|
124
|
+
- [ ] **Step 1: Add `EnumParam` and extend `Enum`/`EnumVariant` in `ast.rs`**
|
|
125
|
+
|
|
126
|
+
Read the current `Enum`/`EnumVariant` structs first (they may have shifted slightly). Add:
|
|
127
|
+
|
|
128
|
+
```rust
|
|
129
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
130
|
+
pub struct EnumParam {
|
|
131
|
+
pub name: String,
|
|
132
|
+
pub ty: Type,
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Extend `Enum`:
|
|
137
|
+
|
|
138
|
+
```rust
|
|
139
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
140
|
+
pub struct Enum {
|
|
141
|
+
pub name: String,
|
|
142
|
+
pub params: Vec<EnumParam>, // new; empty for an enum declared without "(...)"
|
|
143
|
+
pub variants: Vec<EnumVariant>,
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Extend `EnumVariant`:
|
|
148
|
+
|
|
149
|
+
```rust
|
|
150
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
151
|
+
pub struct EnumVariant {
|
|
152
|
+
pub name: String,
|
|
153
|
+
pub fields: Vec<String>, // existing, unchanged meaning
|
|
154
|
+
pub values: Vec<Expr>, // new; empty for an ordinary/generic variant
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
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.
|
|
159
|
+
|
|
160
|
+
- [ ] **Step 2: Parse `enum`'s new `params` field and `enum_field`'s value-list alternative**
|
|
161
|
+
|
|
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`).
|
|
163
|
+
|
|
164
|
+
- [ ] **Step 3: Add a parser test**
|
|
165
|
+
|
|
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.
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 4: Run `plum-core`'s tests**
|
|
169
|
+
|
|
170
|
+
Run: `cargo test -p plum-core 2>&1 | tail -60`
|
|
171
|
+
Expected: PASS.
|
|
172
|
+
|
|
173
|
+
- [ ] **Step 5: Commit**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git add plum-core/src/ast.rs plum-core/src/parser.rs plum-core/tests/parser_test.rs
|
|
177
|
+
git commit -m "feat(plum-core): parse enum discriminant-value declarations"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Task 3: `plum-checker` — validate discriminant values and enable field access
|
|
183
|
+
|
|
184
|
+
**Files:**
|
|
185
|
+
- Modify: `plum-checker/src/lib.rs`
|
|
186
|
+
- Modify: `plum-checker/tests/checker_tests.rs`
|
|
187
|
+
|
|
188
|
+
**Interfaces:**
|
|
189
|
+
- Consumes: `Enum.params`/`EnumVariant.values` from Task 2.
|
|
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.
|
|
191
|
+
|
|
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.
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 1: Read `build_global_tables`, `CheckCtx`, `EnumVariantInfo` and the field-access check function first**
|
|
195
|
+
|
|
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).
|
|
197
|
+
|
|
198
|
+
- [ ] **Step 2: Extend `EnumVariantInfo` with a `values` field**
|
|
199
|
+
|
|
200
|
+
```rust
|
|
201
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
202
|
+
pub struct EnumVariantInfo {
|
|
203
|
+
pub enum_name: String,
|
|
204
|
+
pub tag: i32,
|
|
205
|
+
pub field_types: Vec<PlumType>,
|
|
206
|
+
pub values: Vec<ast::Expr>, // new; non-empty only for a discriminant variant
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
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.
|
|
211
|
+
|
|
212
|
+
- [ ] **Step 3: Populate `field_types`/`values` correctly for discriminant enums in `build_global_tables`**
|
|
213
|
+
|
|
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:
|
|
215
|
+
|
|
216
|
+
```rust
|
|
217
|
+
ast::Item::Enum(e) => {
|
|
218
|
+
let shared_field_types: Vec<PlumType> = e.params.iter()
|
|
219
|
+
.map(|p| plum_type_from_ast(&p.ty))
|
|
220
|
+
.collect();
|
|
221
|
+
for (tag, v) in e.variants.iter().enumerate() {
|
|
222
|
+
let field_types = if !e.params.is_empty() {
|
|
223
|
+
shared_field_types.clone()
|
|
224
|
+
} else {
|
|
225
|
+
v.fields.iter()
|
|
226
|
+
.map(|f| plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] }))
|
|
227
|
+
.collect()
|
|
228
|
+
};
|
|
229
|
+
enum_variants.insert(v.name.clone(), EnumVariantInfo {
|
|
230
|
+
enum_name: e.name.clone(),
|
|
231
|
+
tag: tag as i32,
|
|
232
|
+
field_types,
|
|
233
|
+
values: v.values.clone(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 4: Add arity/type validation for discriminant declarations**
|
|
240
|
+
|
|
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).
|
|
242
|
+
|
|
243
|
+
- [ ] **Step 5: Enable field access on a discriminant enum receiver**
|
|
244
|
+
|
|
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):
|
|
246
|
+
|
|
247
|
+
```rust
|
|
248
|
+
/// Field names and types for every discriminant enum's shared params (`enum Foo(n: Int) = ...`),
|
|
249
|
+
/// keyed by the ENUM's name (not a variant name) — e.g. `"Step" -> [("n", TInt)]`. Field
|
|
250
|
+
/// access on a value of this type must load/store at offset `(field_idx + 1) * 8`, NOT
|
|
251
|
+
/// `field_idx * 8` like a class — slot 0 is always the variant's tag.
|
|
252
|
+
pub type EnumParams = BTreeMap<String, Vec<(String, PlumType)>>;
|
|
253
|
+
```
|
|
254
|
+
|
|
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).
|
|
256
|
+
|
|
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`.
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 6: Add checker tests**
|
|
260
|
+
|
|
261
|
+
In `plum-checker/tests/checker_tests.rs`:
|
|
262
|
+
1. The `Step` example type-checks with no errors.
|
|
263
|
+
2. A variant with the wrong number of discriminant values produces an error.
|
|
264
|
+
3. A variant with a wrongly-typed discriminant value (e.g. a `Str` literal where `Int` is declared) produces an error.
|
|
265
|
+
4. A non-empty `values` on a variant of a param-less (ordinary) enum produces an error.
|
|
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.
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 7: Run `plum-checker`'s tests**
|
|
269
|
+
|
|
270
|
+
Run: `cargo test -p plum-checker 2>&1 | tail -100`
|
|
271
|
+
Expected: PASS, including your new tests, and every pre-existing enum/field-access test.
|
|
272
|
+
|
|
273
|
+
- [ ] **Step 8: Commit**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs
|
|
277
|
+
git commit -m "feat(plum-checker): validate enum discriminant values, enable field access on them"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### Task 4: `plum-wasm-codegen` — compile discriminant-enum construction and field access
|
|
283
|
+
|
|
284
|
+
**Files:**
|
|
285
|
+
- Modify: `plum-wasm-codegen/src/lib.rs`
|
|
286
|
+
- Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
|
|
287
|
+
|
|
288
|
+
**Interfaces:**
|
|
289
|
+
- Consumes: `EnumVariantInfo.values`/`field_types` (Task 3), `EnumParams` (Task 3) threaded into `LocalCtx`.
|
|
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.
|
|
291
|
+
|
|
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**
|
|
293
|
+
|
|
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.
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 2: Thread `EnumParams` into `LocalCtx` and its one construction site**
|
|
297
|
+
|
|
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).
|
|
299
|
+
|
|
300
|
+
- [ ] **Step 3: Make a bare discriminant-variant reference construct correctly**
|
|
301
|
+
|
|
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`:
|
|
303
|
+
|
|
304
|
+
```rust
|
|
305
|
+
ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) {
|
|
306
|
+
Some(info) if info.field_types.is_empty() => {
|
|
307
|
+
Instruction::I32Const(info.tag).encode(body);
|
|
308
|
+
}
|
|
309
|
+
Some(info) if !info.values.is_empty() => {
|
|
310
|
+
let synthetic_call = ast::FnCall {
|
|
311
|
+
name: n.clone(),
|
|
312
|
+
args: info.values.iter().cloned().map(ast::Arg::Positional).collect(),
|
|
313
|
+
};
|
|
314
|
+
compile_variant_construction(info, &synthetic_call, expr, body, ctx, state)?;
|
|
315
|
+
}
|
|
316
|
+
Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
|
|
317
|
+
None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
|
|
318
|
+
},
|
|
319
|
+
```
|
|
320
|
+
|
|
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.
|
|
322
|
+
|
|
323
|
+
- [ ] **Step 4: Register a scratch slot for bare discriminant-variant references in the `Collector`**
|
|
324
|
+
|
|
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)`:
|
|
326
|
+
|
|
327
|
+
```rust
|
|
328
|
+
ast::Expr::TypeName(n) => {
|
|
329
|
+
let carries_baked_in_payload = self.cctx.enum_variants.get(n)
|
|
330
|
+
.map(|info| !info.values.is_empty())
|
|
331
|
+
.unwrap_or(false);
|
|
332
|
+
if carries_baked_in_payload {
|
|
333
|
+
let idx = self.next_classcall_slot;
|
|
334
|
+
self.next_classcall_slot += 1;
|
|
335
|
+
self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
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).
|
|
341
|
+
|
|
342
|
+
- [ ] **Step 5: Field access on a discriminant-enum receiver**
|
|
343
|
+
|
|
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).
|
|
345
|
+
|
|
346
|
+
- [ ] **Step 6: Add codegen tests**
|
|
347
|
+
|
|
348
|
+
In `plum-wasm-codegen/tests/codegen_tests.rs`:
|
|
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.
|
|
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.
|
|
351
|
+
|
|
352
|
+
- [ ] **Step 7: Run the codegen tests**
|
|
353
|
+
|
|
354
|
+
Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -100`
|
|
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).
|
|
356
|
+
|
|
357
|
+
- [ ] **Step 8: Run the full workspace suite**
|
|
358
|
+
|
|
359
|
+
Run: `cargo test --workspace 2>&1 | tail -100`
|
|
360
|
+
Expected: all PASS.
|
|
361
|
+
|
|
362
|
+
- [ ] **Step 9: Commit**
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
|
|
366
|
+
git commit -m "feat(plum-wasm-codegen): compile enum discriminant-value construction and field access"
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### Task 5: Final verification
|
|
372
|
+
|
|
373
|
+
**Files:** none (verification only).
|
|
374
|
+
|
|
375
|
+
- [ ] **Step 1: Full workspace test suite**
|
|
376
|
+
|
|
377
|
+
Run: `cargo test --workspace 2>&1 | tail -100`
|
|
378
|
+
Expected: all PASS.
|
|
379
|
+
|
|
380
|
+
- [ ] **Step 2: Tree-sitter corpus suite**
|
|
381
|
+
|
|
382
|
+
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test 2>&1 | tail -60`
|
|
383
|
+
Expected: all PASS.
|
|
384
|
+
|
|
385
|
+
- [ ] **Step 3: No commit needed** — verification only.
|