plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
2cee306
— Peter John
2026-07-19T22:34:02+05:30
docs: add implementation plan for general enum support
docs/superpowers/plans/2026-07-19-general-enum-support.md
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
# General Enum Support 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:** Make arbitrary concrete (non-generic) user-declared `enum`s — both payload-free (`Color = Red | Green | Blue`) and payload-carrying (`Option = Some(Int) | None`, multi-field `Rect(Float, Float)`) — type-check *precisely* and compile to working wasm, for both construction and `match`, generalizing the Bool-only special-casing that exists today.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Every enum value is a single `i32`: a payload-free variant is its small integer tag directly; a payload variant is a bump-heap pointer to `[tag: i32][field0][field1]...` (same bump-alloc/store/load mechanism already used for class instances). `plum-checker`'s `EnumVariants` table gains a tag number and field types per variant so both the checker and `plum-wasm-codegen` can validate/construct/destructure without re-deriving that data from the AST. A small grammar fix is needed first: today's grammar only lets a capitalized callee use *named*-field syntax (`Cat(name: "x")`), so positional variant calls like `Some(v)` don't actually parse as an expression yet.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Rust (workspace: `plum-core`, `plum-checker`, `plum-wasm-codegen`), tree-sitter grammar (`tooling/tree-sitter-plum`, JS), `wasm-encoder`/`wasmparser`/`wasmtime` for codegen tests.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Out of scope: generics monomorphization, multi-subject `match`, any pattern kind beyond bare tag / constructor (these remain the documented "Known gaps"). Do not attempt them here.
|
|
14
|
+
- Enum variant sub-patterns inside a constructor pattern (`Some(v)`, `Pair(a, b)`) are flat bindings or `_` wildcards only — no nested constructor patterns. This matches the README's existing description ("binding its argument") and current `libs/std`/`examples` usage.
|
|
15
|
+
- Follow existing code style exactly: this codebase has no doc-comment scaffolding beyond one-line "why" comments: mirror the terse style already in `plum-checker/src/lib.rs` and `plum-wasm-codegen/src/lib.rs`.
|
|
16
|
+
- Every task must leave `cargo test --workspace` and (where grammar changed) `npx --yes tree-sitter-cli test` green before moving to the next task.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Task 1: Grammar — positional variant-construction calls (`Some(v)`, `Pair(a, b)`)
|
|
21
|
+
|
|
22
|
+
**Files:**
|
|
23
|
+
- Modify: `tooling/tree-sitter-plum/grammar.js:55` (conflicts array), `tooling/tree-sitter-plum/grammar.js:387-394` (`fn_call` rule)
|
|
24
|
+
- Test: `tooling/tree-sitter-plum/test/corpus/function.txt` (append a new case)
|
|
25
|
+
|
|
26
|
+
**Interfaces:**
|
|
27
|
+
- Consumes: nothing from other tasks.
|
|
28
|
+
- Produces: `fn_call` nodes whose `function` field can be a `type_identifier` (not just `var_identifier`). `plum-core`'s `parser.rs::parse_fn_call` (unchanged — it already reads the callee via `self.text(n)` regardless of node kind) will keep parsing these into `ast::FnCall { name, args }` exactly like any other call.
|
|
29
|
+
|
|
30
|
+
Today, `class_call`'s `class_argument_list` only accepts `name: value` pairs (see `type.txt` corpus: `Cat(name: name, age: 0)`), so a positional capitalized call like `Some(v)` or `Ok(x)` does not currently parse as an expression at all (it only exists as a *pattern*, via `class_pattern`). This task adds that missing expression-level syntax by letting `fn_call`'s callee be either a `var_identifier` or a `type_identifier` — the argument-list grammars already disambiguate cleanly (`class_argument_list` requires `name:`, `fn_argument_list` never does), so this doesn't change parsing of any existing `Cat(name: "x")`-style call.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Edit `fn_call` to accept a capitalized callee**
|
|
33
|
+
|
|
34
|
+
In `tooling/tree-sitter-plum/grammar.js`, change:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
fn_call: ($) =>
|
|
38
|
+
prec(PREC.call, seq(
|
|
39
|
+
field("function", $.var_identifier),
|
|
40
|
+
field(
|
|
41
|
+
"arguments",
|
|
42
|
+
$.fn_argument_list,
|
|
43
|
+
),
|
|
44
|
+
)),
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
to:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
fn_call: ($) =>
|
|
51
|
+
prec(PREC.call, seq(
|
|
52
|
+
field("function", choice($.var_identifier, $.type_identifier)),
|
|
53
|
+
field(
|
|
54
|
+
"arguments",
|
|
55
|
+
$.fn_argument_list,
|
|
56
|
+
),
|
|
57
|
+
)),
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- [ ] **Step 2: Declare the `fn_call`/`class_call` conflict and regenerate**
|
|
61
|
+
|
|
62
|
+
In the same file, change line 55 from:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
conflicts: ($) => [],
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
to:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
conflicts: ($) => [[$.fn_call, $.class_call]],
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Expected: completes with no "Unresolved conflict" errors. (If it reports one anyway, that means the two rules are ambiguous on some input neither of us anticipated — read the reported example carefully before changing anything further; don't just silence it.)
|
|
81
|
+
|
|
82
|
+
- [ ] **Step 3: Add the input half of a new corpus case**
|
|
83
|
+
|
|
84
|
+
Append to `tooling/tree-sitter-plum/test/corpus/function.txt`:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
================================================================================
|
|
88
|
+
function - variant construction call (positional args on a capitalized name)
|
|
89
|
+
================================================================================
|
|
90
|
+
|
|
91
|
+
makeSome(v: Int) -> Option =
|
|
92
|
+
Some(v)
|
|
93
|
+
|
|
94
|
+
--------------------------------------------------------------------------------
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
(Leave the expected-tree section empty for now — the next step generates it.)
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 4: Generate the expected tree and verify it**
|
|
100
|
+
|
|
101
|
+
Run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test -u -f "variant construction call"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Expected: the tool fills in the tree under the separator. Open `test/corpus/function.txt` and confirm the generated tree is `(source (fn (fn_identifier) (param (var_identifier) (type (type_identifier))) (return_type (type_identifier)) (body (primary_expression (fn_call (type_identifier) (fn_argument_list (expression (primary_expression (var_identifier)))))))))` (formatted one-node-per-line as the rest of the corpus already is) — i.e. the callee shows up as `(type_identifier)` inside `fn_call`, not `class_call`, and there is no `ERROR`/`MISSING` node anywhere.
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 5: Run the full corpus suite**
|
|
110
|
+
|
|
111
|
+
Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test`
|
|
112
|
+
Expected: all cases pass, including every pre-existing `class_call` case in `type.txt` (proving the grammar change didn't regress named-field construction).
|
|
113
|
+
|
|
114
|
+
- [ ] **Step 6: Commit**
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/test/corpus/function.txt
|
|
118
|
+
git add tooling/tree-sitter-plum/src # generated parser.c etc, if tracked
|
|
119
|
+
git commit -m "feat(tree-sitter-plum): allow positional variant-construction calls"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
(If `tooling/tree-sitter-plum/src/parser.c` and friends are `.gitignore`d, drop that second `git add` — check `git status` first.)
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### Task 2: Checker — per-variant tag + field types, and a real construction/pattern type check
|
|
127
|
+
|
|
128
|
+
**Files:**
|
|
129
|
+
- Modify: `plum-checker/src/lib.rs:37-492`
|
|
130
|
+
- Test: `plum-checker/tests/checker_tests.rs`
|
|
131
|
+
|
|
132
|
+
**Interfaces:**
|
|
133
|
+
- Consumes: nothing from Task 1 at the type level (checker doesn't care about *how* an expression parsed, only its `ast::Expr` shape) — but Task 1 is what makes `Some(v)`-as-`FnCall` reach the checker at all.
|
|
134
|
+
- Produces:
|
|
135
|
+
```rust
|
|
136
|
+
pub struct EnumVariantInfo {
|
|
137
|
+
pub enum_name: String,
|
|
138
|
+
pub tag: i32,
|
|
139
|
+
pub field_types: Vec<PlumType>,
|
|
140
|
+
}
|
|
141
|
+
pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
|
|
142
|
+
```
|
|
143
|
+
used by Tasks 3 and 4 (`plum-wasm-codegen`) via `ctx.enum_variants.get(name)`.
|
|
144
|
+
|
|
145
|
+
- [ ] **Step 1: Write failing checker tests for the new behavior**
|
|
146
|
+
|
|
147
|
+
Append to `plum-checker/tests/checker_tests.rs`:
|
|
148
|
+
|
|
149
|
+
```rust
|
|
150
|
+
#[test]
|
|
151
|
+
fn bare_enum_tag_unifies_with_owning_enum_type() {
|
|
152
|
+
// Regression: a bare non-Bool tag like `None` used to type as `TNamed("None")`
|
|
153
|
+
// (itself, not its enum), so comparing it against an `Option` value would wrongly
|
|
154
|
+
// fail with a type mismatch.
|
|
155
|
+
let src = "\
|
|
156
|
+
enum Option =
|
|
157
|
+
| Some(Int)
|
|
158
|
+
| None
|
|
159
|
+
|
|
160
|
+
isNone(o: Option) -> Bool =
|
|
161
|
+
o == None
|
|
162
|
+
";
|
|
163
|
+
let source = parse(src);
|
|
164
|
+
let result = check_source(&source);
|
|
165
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn variant_construction_checks_arg_count_and_types() {
|
|
170
|
+
let src = "\
|
|
171
|
+
enum Option =
|
|
172
|
+
| Some(Int)
|
|
173
|
+
| None
|
|
174
|
+
|
|
175
|
+
makeSome(v: Int) -> Option =
|
|
176
|
+
Some(v)
|
|
177
|
+
";
|
|
178
|
+
let source = parse(src);
|
|
179
|
+
let result = check_source(&source);
|
|
180
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn variant_construction_wrong_arg_type_is_error() {
|
|
185
|
+
let src = "\
|
|
186
|
+
enum Option =
|
|
187
|
+
| Some(Int)
|
|
188
|
+
| None
|
|
189
|
+
|
|
190
|
+
bad() -> Option =
|
|
191
|
+
Some(\"x\")
|
|
192
|
+
";
|
|
193
|
+
let source = parse(src);
|
|
194
|
+
let result = check_source(&source);
|
|
195
|
+
assert!(result.is_err());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn variant_construction_wrong_arg_count_is_error() {
|
|
200
|
+
let src = "\
|
|
201
|
+
enum Shape =
|
|
202
|
+
| Rect(Float, Float)
|
|
203
|
+
| Circle(Float)
|
|
204
|
+
|
|
205
|
+
bad() -> Shape =
|
|
206
|
+
Rect(1.0)
|
|
207
|
+
";
|
|
208
|
+
let source = parse(src);
|
|
209
|
+
let result = check_source(&source);
|
|
210
|
+
assert!(result.is_err());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn constructor_pattern_binds_fields_to_declared_types() {
|
|
215
|
+
let src = "\
|
|
216
|
+
enum Shape =
|
|
217
|
+
| Rect(Float, Float)
|
|
218
|
+
| Circle(Float)
|
|
219
|
+
|
|
220
|
+
area(s: Shape) -> Float =
|
|
221
|
+
match s
|
|
222
|
+
Rect(w, h) =>
|
|
223
|
+
w * h
|
|
224
|
+
Circle(r) =>
|
|
225
|
+
r * r
|
|
226
|
+
";
|
|
227
|
+
let source = parse(src);
|
|
228
|
+
let result = check_source(&source);
|
|
229
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn constructor_pattern_wrong_field_count_is_error() {
|
|
234
|
+
let src = "\
|
|
235
|
+
enum Shape =
|
|
236
|
+
| Rect(Float, Float)
|
|
237
|
+
| Circle(Float)
|
|
238
|
+
|
|
239
|
+
bad(s: Shape) -> Float =
|
|
240
|
+
match s
|
|
241
|
+
Rect(w) =>
|
|
242
|
+
w
|
|
243
|
+
_ =>
|
|
244
|
+
0.0
|
|
245
|
+
";
|
|
246
|
+
let source = parse(src);
|
|
247
|
+
let result = check_source(&source);
|
|
248
|
+
assert!(result.is_err());
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- [ ] **Step 2: Run the tests to see them fail**
|
|
253
|
+
|
|
254
|
+
Run: `cargo test -p plum-checker --test checker_tests`
|
|
255
|
+
Expected: `bare_enum_tag_unifies_with_owning_enum_type` fails (type mismatch: `Option` vs `None`); `variant_construction_wrong_arg_count_is_error` and `constructor_pattern_wrong_field_count_is_error` fail (currently these type-check permissively as `Ok`, since arg/field counts aren't validated yet); the other three currently happen to pass already (permissive fallback) — that's fine, they'll keep passing once implemented properly.
|
|
256
|
+
|
|
257
|
+
- [ ] **Step 3: Add `EnumVariantInfo` and rebuild `EnumVariants`**
|
|
258
|
+
|
|
259
|
+
In `plum-checker/src/lib.rs`, replace line 47-48:
|
|
260
|
+
|
|
261
|
+
```rust
|
|
262
|
+
/// Enum variant name -> owning enum name, e.g. `"True" -> "Bool"`.
|
|
263
|
+
pub type EnumVariants = BTreeMap<String, String>;
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
with:
|
|
267
|
+
|
|
268
|
+
```rust
|
|
269
|
+
/// Info about one `enum` variant: which enum it belongs to, its 0-based runtime tag
|
|
270
|
+
/// (numbering is shared across all of that enum's variants), and its payload field
|
|
271
|
+
/// types (empty for a payload-free variant like `Red` or `None`).
|
|
272
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
273
|
+
pub struct EnumVariantInfo {
|
|
274
|
+
pub enum_name: String,
|
|
275
|
+
pub tag: i32,
|
|
276
|
+
pub field_types: Vec<PlumType>,
|
|
277
|
+
}
|
|
278
|
+
/// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`.
|
|
279
|
+
pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Replace lines 66-70:
|
|
283
|
+
|
|
284
|
+
```rust
|
|
285
|
+
let mut enum_variants: EnumVariants = BTreeMap::new();
|
|
286
|
+
// `Bool`'s variants are built in (see `infer_expr`'s TypeName handling) rather
|
|
287
|
+
// than requiring every source file to redeclare `enum Bool = | True | False`.
|
|
288
|
+
enum_variants.insert("True".to_string(), "Bool".to_string());
|
|
289
|
+
enum_variants.insert("False".to_string(), "Bool".to_string());
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
with:
|
|
293
|
+
|
|
294
|
+
```rust
|
|
295
|
+
let mut enum_variants: EnumVariants = BTreeMap::new();
|
|
296
|
+
// `Bool`'s variants are built in (see `infer_expr`'s TypeName handling) rather
|
|
297
|
+
// than requiring every source file to redeclare `enum Bool = | True | False`.
|
|
298
|
+
enum_variants.insert("True".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 1, field_types: vec![] });
|
|
299
|
+
enum_variants.insert("False".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 0, field_types: vec![] });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Replace lines 82-86:
|
|
303
|
+
|
|
304
|
+
```rust
|
|
305
|
+
ast::Item::Enum(e) => {
|
|
306
|
+
for v in &e.variants {
|
|
307
|
+
enum_variants.insert(v.name.clone(), e.name.clone());
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
with:
|
|
313
|
+
|
|
314
|
+
```rust
|
|
315
|
+
ast::Item::Enum(e) => {
|
|
316
|
+
for (tag, v) in e.variants.iter().enumerate() {
|
|
317
|
+
let field_types = v.fields.iter()
|
|
318
|
+
.map(|f| plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] }))
|
|
319
|
+
.collect();
|
|
320
|
+
enum_variants.insert(v.name.clone(), EnumVariantInfo {
|
|
321
|
+
enum_name: e.name.clone(),
|
|
322
|
+
tag: tag as i32,
|
|
323
|
+
field_types,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- [ ] **Step 4: Fix `TypeName` to type as the owning enum, not itself**
|
|
330
|
+
|
|
331
|
+
Replace lines 368-371:
|
|
332
|
+
|
|
333
|
+
```rust
|
|
334
|
+
ast::Expr::TypeName(n) => match n.as_str() {
|
|
335
|
+
"True" | "False" => Ok(PlumType::TBool),
|
|
336
|
+
other => Ok(PlumType::TNamed(other.to_string())),
|
|
337
|
+
},
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
with:
|
|
341
|
+
|
|
342
|
+
```rust
|
|
343
|
+
ast::Expr::TypeName(n) => match n.as_str() {
|
|
344
|
+
"True" | "False" => Ok(PlumType::TBool),
|
|
345
|
+
_ => match ctx.enum_variants.get(n) {
|
|
346
|
+
Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
|
|
347
|
+
// Unmodeled/builtin type name: allow, codegen will catch.
|
|
348
|
+
None => Ok(PlumType::TNamed(n.to_string())),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- [ ] **Step 5: Type-check variant-construction `FnCall`s properly**
|
|
354
|
+
|
|
355
|
+
Replace lines 409-429:
|
|
356
|
+
|
|
357
|
+
```rust
|
|
358
|
+
ast::Expr::FnCall(call) => {
|
|
359
|
+
match lookup(env, &call.name) {
|
|
360
|
+
Ok(PlumType::TFun(param_types, ret)) => {
|
|
361
|
+
if call.args.len() != param_types.len() {
|
|
362
|
+
return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
|
|
363
|
+
}
|
|
364
|
+
for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
|
|
365
|
+
let arg_expr = match arg {
|
|
366
|
+
ast::Arg::Positional(e) => e,
|
|
367
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
368
|
+
ast::Arg::Pair { value, .. } => value,
|
|
369
|
+
};
|
|
370
|
+
let actual = infer_expr(arg_expr, env, ctx)?;
|
|
371
|
+
unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
|
|
372
|
+
}
|
|
373
|
+
Ok(*ret)
|
|
374
|
+
}
|
|
375
|
+
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
|
376
|
+
Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
with:
|
|
382
|
+
|
|
383
|
+
```rust
|
|
384
|
+
ast::Expr::FnCall(call) => {
|
|
385
|
+
if let Some(info) = ctx.enum_variants.get(&call.name) {
|
|
386
|
+
if call.args.len() != info.field_types.len() {
|
|
387
|
+
return Err(format!(
|
|
388
|
+
"variant '{}': expected {} arg(s), got {}",
|
|
389
|
+
call.name, info.field_types.len(), call.args.len()
|
|
390
|
+
));
|
|
391
|
+
}
|
|
392
|
+
for (i, (arg, expected)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
|
|
393
|
+
let arg_expr = match arg {
|
|
394
|
+
ast::Arg::Positional(e) => e,
|
|
395
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
396
|
+
ast::Arg::Pair { value, .. } => value,
|
|
397
|
+
};
|
|
398
|
+
let actual = infer_expr(arg_expr, env, ctx)?;
|
|
399
|
+
unify(expected, &actual).map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?;
|
|
400
|
+
}
|
|
401
|
+
return Ok(PlumType::TNamed(info.enum_name.clone()));
|
|
402
|
+
}
|
|
403
|
+
match lookup(env, &call.name) {
|
|
404
|
+
Ok(PlumType::TFun(param_types, ret)) => {
|
|
405
|
+
if call.args.len() != param_types.len() {
|
|
406
|
+
return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
|
|
407
|
+
}
|
|
408
|
+
for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
|
|
409
|
+
let arg_expr = match arg {
|
|
410
|
+
ast::Arg::Positional(e) => e,
|
|
411
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
412
|
+
ast::Arg::Pair { value, .. } => value,
|
|
413
|
+
};
|
|
414
|
+
let actual = infer_expr(arg_expr, env, ctx)?;
|
|
415
|
+
unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
|
|
416
|
+
}
|
|
417
|
+
Ok(*ret)
|
|
418
|
+
}
|
|
419
|
+
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
|
420
|
+
Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
- [ ] **Step 6: Type-check constructor patterns against real field types**
|
|
426
|
+
|
|
427
|
+
Replace the doc comment and body at lines 333-359:
|
|
428
|
+
|
|
429
|
+
```rust
|
|
430
|
+
/// Checks a single case pattern against the type of the subject it matches, binding any
|
|
431
|
+
/// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind
|
|
432
|
+
/// against an unconstrained type since enum variants don't carry per-field type info (v1.5).
|
|
433
|
+
fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> {
|
|
434
|
+
match pat {
|
|
435
|
+
ast::CasePattern::Wildcard => Ok(()),
|
|
436
|
+
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt),
|
|
437
|
+
ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat),
|
|
438
|
+
ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr),
|
|
439
|
+
ast::CasePattern::Name(n) => {
|
|
440
|
+
let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
441
|
+
&& ctx.enum_variants.contains_key(n);
|
|
442
|
+
if is_known_variant {
|
|
443
|
+
Ok(()) // equality check against a known enum tag, e.g. `True`
|
|
444
|
+
} else {
|
|
445
|
+
env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
|
|
446
|
+
Ok(())
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
ast::CasePattern::Class { name: _, fields } => {
|
|
450
|
+
for f in fields {
|
|
451
|
+
check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
|
|
452
|
+
}
|
|
453
|
+
Ok(())
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
with:
|
|
460
|
+
|
|
461
|
+
```rust
|
|
462
|
+
/// Checks a single case pattern against the type of the subject it matches, binding any
|
|
463
|
+
/// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind
|
|
464
|
+
/// against that variant's declared field types (see `EnumVariantInfo::field_types`).
|
|
465
|
+
fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> {
|
|
466
|
+
match pat {
|
|
467
|
+
ast::CasePattern::Wildcard => Ok(()),
|
|
468
|
+
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt),
|
|
469
|
+
ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat),
|
|
470
|
+
ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr),
|
|
471
|
+
ast::CasePattern::Name(n) => {
|
|
472
|
+
let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
473
|
+
&& ctx.enum_variants.contains_key(n);
|
|
474
|
+
if is_known_variant {
|
|
475
|
+
Ok(()) // equality check against a known enum tag, e.g. `True`
|
|
476
|
+
} else {
|
|
477
|
+
env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
|
|
478
|
+
Ok(())
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) {
|
|
482
|
+
Some(info) => {
|
|
483
|
+
if fields.len() != info.field_types.len() {
|
|
484
|
+
return Err(format!(
|
|
485
|
+
"constructor pattern '{}' expects {} field(s), got {}",
|
|
486
|
+
name, info.field_types.len(), fields.len()
|
|
487
|
+
));
|
|
488
|
+
}
|
|
489
|
+
for (f, fty) in fields.iter().zip(info.field_types.iter()) {
|
|
490
|
+
check_pattern(f, fty, env, ctx)?;
|
|
491
|
+
}
|
|
492
|
+
Ok(())
|
|
493
|
+
}
|
|
494
|
+
// Unmodeled/builtin variant: allow, codegen will catch.
|
|
495
|
+
None => {
|
|
496
|
+
for f in fields {
|
|
497
|
+
check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
|
|
498
|
+
}
|
|
499
|
+
Ok(())
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
- [ ] **Step 7: Run checker tests**
|
|
507
|
+
|
|
508
|
+
Run: `cargo test -p plum-checker --test checker_tests`
|
|
509
|
+
Expected: all pass, including the six new tests.
|
|
510
|
+
|
|
511
|
+
- [ ] **Step 8: Run the full checker crate + examples test**
|
|
512
|
+
|
|
513
|
+
Run: `cargo test -p plum-checker`
|
|
514
|
+
Expected: all pass (this includes `examples_test.rs`, which type-checks every file under `examples/` — `types.plum` and `match.plum` both already declare a concrete `Option`/`Color` enum, so this proves the fix doesn't regress them).
|
|
515
|
+
|
|
516
|
+
- [ ] **Step 9: Commit**
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs
|
|
520
|
+
git commit -m "feat(plum-checker): general enum variant tags, field types, and construction checks"
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
### Task 3: Codegen — variant construction (`Some(v)`, `Pair(a, b)` compile to a tagged heap struct)
|
|
526
|
+
|
|
527
|
+
**Files:**
|
|
528
|
+
- Modify: `plum-wasm-codegen/src/lib.rs:1-10` (import), `:450-500` (`Collector::walk_expr`), `:1040-1054` (`compile_expr`'s `FnCall` arm)
|
|
529
|
+
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
|
|
530
|
+
|
|
531
|
+
**Interfaces:**
|
|
532
|
+
- Consumes: `plum_checker::EnumVariantInfo` (Task 2) via `ctx.enum_variants.get(&call.name)` — `EnumVariantInfo { enum_name: String, tag: i32, field_types: Vec<PlumType> }`.
|
|
533
|
+
- Produces: a payload-free variant call/bare value compiles to `i32.const <tag>`; a payload variant call compiles to a bump-allocated `[tag][field0][field1]...` struct (8-byte stride per slot, same convention as class fields) with its base pointer left on the stack. Task 4 (match lowering) reads this same layout back out.
|
|
534
|
+
|
|
535
|
+
- [ ] **Step 1: Write failing codegen tests**
|
|
536
|
+
|
|
537
|
+
Append to `plum-wasm-codegen/tests/codegen_tests.rs`:
|
|
538
|
+
|
|
539
|
+
```rust
|
|
540
|
+
#[test]
|
|
541
|
+
fn payload_free_variant_construction_compiles() {
|
|
542
|
+
let src = "\
|
|
543
|
+
enum Color =
|
|
544
|
+
| Red
|
|
545
|
+
| Green
|
|
546
|
+
| Blue
|
|
547
|
+
|
|
548
|
+
main() -> Int =\n x = Green\n 0\n";
|
|
549
|
+
assert_valid(src);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#[test]
|
|
553
|
+
fn payload_variant_construction_compiles_and_runs() {
|
|
554
|
+
let src = "\
|
|
555
|
+
enum Option =
|
|
556
|
+
| Some(Int)
|
|
557
|
+
| None
|
|
558
|
+
|
|
559
|
+
unwrapOr(o: Option, default: Int) -> Int =
|
|
560
|
+
match o
|
|
561
|
+
Some(v) =>
|
|
562
|
+
return v
|
|
563
|
+
None =>
|
|
564
|
+
return default
|
|
565
|
+
|
|
566
|
+
main() -> Int =
|
|
567
|
+
unwrapOr(Some(7), 0)
|
|
568
|
+
";
|
|
569
|
+
let source = parse(src);
|
|
570
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
571
|
+
assert_eq!(run_main(&bytes), 7);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[test]
|
|
575
|
+
fn multi_field_variant_construction_compiles_and_runs() {
|
|
576
|
+
let src = "\
|
|
577
|
+
enum Shape =
|
|
578
|
+
| Rect(Int, Int)
|
|
579
|
+
| Circle(Int)
|
|
580
|
+
|
|
581
|
+
area(s: Shape) -> Int =
|
|
582
|
+
match s
|
|
583
|
+
Rect(w, h) =>
|
|
584
|
+
return w * h
|
|
585
|
+
Circle(r) =>
|
|
586
|
+
return r * r
|
|
587
|
+
|
|
588
|
+
main() -> Int =
|
|
589
|
+
area(Rect(3, 4))
|
|
590
|
+
";
|
|
591
|
+
let source = parse(src);
|
|
592
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
593
|
+
assert_eq!(run_main(&bytes), 12);
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
(These also exercise Task 4's match lowering — that's expected; construction and destructuring are tested together since one is useless to test without the other. Task 4 will make the `Some`/`None`/`Rect`/`Circle` match arms actually compile.)
|
|
598
|
+
|
|
599
|
+
- [ ] **Step 2: Run to see them fail**
|
|
600
|
+
|
|
601
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
|
|
602
|
+
Expected: `payload_free_variant_construction_compiles` fails with `codegen: type name 'Green' is not yet supported as a value` (bare non-Bool `TypeName` isn't handled yet — that's fixed in Step 4 below); the other two fail with `codegen: enum variant pattern '...' is not yet supported (only True/False)` (Task 4's job) — confirm both failure modes appear, then proceed.
|
|
603
|
+
|
|
604
|
+
- [ ] **Step 3: Import `EnumVariantInfo`**
|
|
605
|
+
|
|
606
|
+
In `plum-wasm-codegen/src/lib.rs`, change line 6:
|
|
607
|
+
|
|
608
|
+
```rust
|
|
609
|
+
use plum_checker::{ClassEnv, MethodEnv, EnumVariants};
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
to:
|
|
613
|
+
|
|
614
|
+
```rust
|
|
615
|
+
use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo};
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
- [ ] **Step 4: Make bare payload-free variants compile as their tag**
|
|
619
|
+
|
|
620
|
+
Replace lines 1063-1067:
|
|
621
|
+
|
|
622
|
+
```rust
|
|
623
|
+
ast::Expr::TypeName(n) => match n.as_str() {
|
|
624
|
+
"True" => Instruction::I32Const(1).encode(body),
|
|
625
|
+
"False" => Instruction::I32Const(0).encode(body),
|
|
626
|
+
other => return Err(format!("codegen: type name '{}' is not yet supported as a value", other)),
|
|
627
|
+
},
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
with:
|
|
631
|
+
|
|
632
|
+
```rust
|
|
633
|
+
ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) {
|
|
634
|
+
Some(info) if info.field_types.is_empty() => {
|
|
635
|
+
Instruction::I32Const(info.tag).encode(body);
|
|
636
|
+
}
|
|
637
|
+
Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
|
|
638
|
+
None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
|
|
639
|
+
},
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
- [ ] **Step 5: Allocate a scratch slot for payload-variant construction in `Collector`**
|
|
643
|
+
|
|
644
|
+
In `Collector::walk_expr`, replace lines 480-484:
|
|
645
|
+
|
|
646
|
+
```rust
|
|
647
|
+
ast::Expr::FnCall(call) => {
|
|
648
|
+
for arg in &call.args {
|
|
649
|
+
self.walk_arg(arg);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
with:
|
|
655
|
+
|
|
656
|
+
```rust
|
|
657
|
+
ast::Expr::FnCall(call) => {
|
|
658
|
+
let carries_payload = self.cctx.enum_variants.get(&call.name)
|
|
659
|
+
.map(|info| !info.field_types.is_empty())
|
|
660
|
+
.unwrap_or(false);
|
|
661
|
+
if carries_payload {
|
|
662
|
+
let idx = self.next_classcall_slot;
|
|
663
|
+
self.next_classcall_slot += 1;
|
|
664
|
+
self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
|
|
665
|
+
}
|
|
666
|
+
for arg in &call.args {
|
|
667
|
+
self.walk_arg(arg);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
(`classcall_scratch`/`next_classcall_slot` are the same pool `ClassCall` already uses — a scratch local slot per allocation-then-store expression. Both kinds of expression have distinct pointer identities, so sharing the pool is safe: no key collisions.)
|
|
673
|
+
|
|
674
|
+
- [ ] **Step 6: Compile payload-variant construction in `compile_expr`**
|
|
675
|
+
|
|
676
|
+
Replace lines 1040-1054:
|
|
677
|
+
|
|
678
|
+
```rust
|
|
679
|
+
ast::Expr::FnCall(call) => {
|
|
680
|
+
for arg in &call.args {
|
|
681
|
+
let arg_expr = match arg {
|
|
682
|
+
ast::Arg::Positional(e) => e,
|
|
683
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
684
|
+
ast::Arg::Pair { value, .. } => value,
|
|
685
|
+
};
|
|
686
|
+
compile_expr(arg_expr, body, ctx, state)?;
|
|
687
|
+
}
|
|
688
|
+
let func_idx = ctx
|
|
689
|
+
.func_ids
|
|
690
|
+
.get(&call.name)
|
|
691
|
+
.ok_or_else(|| format!("unknown function '{}'", call.name))?;
|
|
692
|
+
Instruction::Call(*func_idx).encode(body);
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
with:
|
|
697
|
+
|
|
698
|
+
```rust
|
|
699
|
+
ast::Expr::FnCall(call) => {
|
|
700
|
+
if let Some(info) = ctx.enum_variants.get(&call.name) {
|
|
701
|
+
compile_variant_construction(info, call, expr, body, ctx, state)?;
|
|
702
|
+
} else {
|
|
703
|
+
for arg in &call.args {
|
|
704
|
+
let arg_expr = match arg {
|
|
705
|
+
ast::Arg::Positional(e) => e,
|
|
706
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
707
|
+
ast::Arg::Pair { value, .. } => value,
|
|
708
|
+
};
|
|
709
|
+
compile_expr(arg_expr, body, ctx, state)?;
|
|
710
|
+
}
|
|
711
|
+
let func_idx = ctx
|
|
712
|
+
.func_ids
|
|
713
|
+
.get(&call.name)
|
|
714
|
+
.ok_or_else(|| format!("unknown function '{}'", call.name))?;
|
|
715
|
+
Instruction::Call(*func_idx).encode(body);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Then add this new function right after `compile_expr`'s closing brace (after line ~1178, before `plum_type_from_valtype_hint` or anywhere else at module scope):
|
|
721
|
+
|
|
722
|
+
```rust
|
|
723
|
+
/// Compiles a variant-construction call. A payload-free variant (`None`, called as
|
|
724
|
+
/// `None()` rather than used bare) is just its tag. A payload variant bump-allocates
|
|
725
|
+
/// `[tag: i32][field0][field1]...` (8-byte stride per slot, matching class field
|
|
726
|
+
/// layout) and leaves the base pointer on the stack.
|
|
727
|
+
fn compile_variant_construction(
|
|
728
|
+
info: &EnumVariantInfo,
|
|
729
|
+
call: &ast::FnCall,
|
|
730
|
+
expr: &ast::Expr,
|
|
731
|
+
body: &mut Vec<u8>,
|
|
732
|
+
ctx: &LocalCtx,
|
|
733
|
+
state: &mut ModuleState,
|
|
734
|
+
) -> Result<(), String> {
|
|
735
|
+
if call.args.len() != info.field_types.len() {
|
|
736
|
+
return Err(format!(
|
|
737
|
+
"codegen: variant '{}' expects {} arg(s), got {}",
|
|
738
|
+
call.name, info.field_types.len(), call.args.len()
|
|
739
|
+
));
|
|
740
|
+
}
|
|
741
|
+
if info.field_types.is_empty() {
|
|
742
|
+
Instruction::I32Const(info.tag).encode(body);
|
|
743
|
+
return Ok(());
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let size = (1 + info.field_types.len() as i32) * 8;
|
|
747
|
+
let scratch_key = expr as *const ast::Expr as usize;
|
|
748
|
+
let scratch_idx = *ctx
|
|
749
|
+
.classcall_scratch
|
|
750
|
+
.get(&scratch_key)
|
|
751
|
+
.ok_or_else(|| "internal codegen error: missing variant-call scratch slot".to_string())?;
|
|
752
|
+
let scratch_local = ctx.classcall_scratch_base + scratch_idx;
|
|
753
|
+
|
|
754
|
+
Instruction::GlobalGet(ctx.bump_global).encode(body);
|
|
755
|
+
Instruction::LocalSet(scratch_local).encode(body);
|
|
756
|
+
Instruction::GlobalGet(ctx.bump_global).encode(body);
|
|
757
|
+
Instruction::I32Const(size).encode(body);
|
|
758
|
+
Instruction::I32Add.encode(body);
|
|
759
|
+
Instruction::GlobalSet(ctx.bump_global).encode(body);
|
|
760
|
+
|
|
761
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
762
|
+
Instruction::I32Const(info.tag).encode(body);
|
|
763
|
+
Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
|
|
764
|
+
|
|
765
|
+
for (i, (arg, field_ty)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
|
|
766
|
+
let arg_expr = match arg {
|
|
767
|
+
ast::Arg::Positional(e) => e,
|
|
768
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
769
|
+
ast::Arg::Pair { value, .. } => value,
|
|
770
|
+
};
|
|
771
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
772
|
+
compile_expr(arg_expr, body, ctx, state)?;
|
|
773
|
+
let offset = ((i + 1) as u64) * 8;
|
|
774
|
+
match plum_type_to_valtype(field_ty) {
|
|
775
|
+
ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
|
|
776
|
+
ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
|
|
777
|
+
_ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
781
|
+
Ok(())
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 7: Run codegen tests**
|
|
786
|
+
|
|
787
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
|
|
788
|
+
Expected: `payload_free_variant_construction_compiles` now passes. The other two new tests still fail (match lowering isn't done yet — that's Task 4); confirm they now fail specifically inside `match`, not during construction (temporarily comment out their `match` bodies and replace with a literal return if you want to isolate-verify construction alone; then restore).
|
|
789
|
+
|
|
790
|
+
- [ ] **Step 8: Commit**
|
|
791
|
+
|
|
792
|
+
```bash
|
|
793
|
+
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
|
|
794
|
+
git commit -m "feat(plum-wasm-codegen): compile general enum variant construction"
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
### Task 4: Codegen — match lowering for general enum tags and constructor patterns
|
|
800
|
+
|
|
801
|
+
**Files:**
|
|
802
|
+
- Modify: `plum-wasm-codegen/src/lib.rs:420-445` (`Collector::walk_stmt`'s `Match` arm), `:844-929` (`compile_match_arms` / `compile_variant_eq_arm`)
|
|
803
|
+
- Test: `plum-wasm-codegen/tests/codegen_tests.rs` (Task 3's `payload_variant_construction_compiles_and_runs` and `multi_field_variant_construction_compiles_and_runs` will pass once this lands)
|
|
804
|
+
|
|
805
|
+
**Interfaces:**
|
|
806
|
+
- Consumes: `EnumVariantInfo` (Task 2), the bump-heap `[tag][field...]` layout (Task 3).
|
|
807
|
+
- Produces: nothing further downstream — this is the last piece of the feature.
|
|
808
|
+
|
|
809
|
+
- [ ] **Step 1: Confirm Task 3's two match-dependent tests still fail, for the right reason**
|
|
810
|
+
|
|
811
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests payload_variant_construction_compiles_and_runs multi_field_variant_construction_compiles_and_runs`
|
|
812
|
+
Expected: both fail with `codegen: enum variant pattern '...' is not yet supported (only True/False)` or `codegen: constructor match patterns are not yet supported`.
|
|
813
|
+
|
|
814
|
+
- [ ] **Step 2: Bind constructor-pattern fields to their real types in `Collector`**
|
|
815
|
+
|
|
816
|
+
Replace lines 431-444:
|
|
817
|
+
|
|
818
|
+
```rust
|
|
819
|
+
for case in &m.cases {
|
|
820
|
+
let saved = self.env.clone();
|
|
821
|
+
if m.subjects.len() == 1 {
|
|
822
|
+
if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
|
|
823
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
824
|
+
&& self.cctx.enum_variants.contains_key(n);
|
|
825
|
+
if !is_variant {
|
|
826
|
+
self.bind(n, subject_ty.clone());
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
self.walk_block(&case.body);
|
|
831
|
+
self.env = saved;
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
with:
|
|
836
|
+
|
|
837
|
+
```rust
|
|
838
|
+
for case in &m.cases {
|
|
839
|
+
let saved = self.env.clone();
|
|
840
|
+
if m.subjects.len() == 1 {
|
|
841
|
+
match case.patterns.first() {
|
|
842
|
+
Some(ast::CasePattern::Name(n)) => {
|
|
843
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
844
|
+
&& self.cctx.enum_variants.contains_key(n);
|
|
845
|
+
if !is_variant {
|
|
846
|
+
self.bind(n, subject_ty.clone());
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
Some(ast::CasePattern::Class { name, fields }) => {
|
|
850
|
+
if let Some(info) = self.cctx.enum_variants.get(name) {
|
|
851
|
+
let field_types = info.field_types.clone();
|
|
852
|
+
for (f, fty) in fields.iter().zip(field_types.iter()) {
|
|
853
|
+
if let ast::CasePattern::Name(n) = f {
|
|
854
|
+
self.bind(n, fty.clone());
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
_ => {}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
self.walk_block(&case.body);
|
|
863
|
+
self.env = saved;
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
- [ ] **Step 3: Generalize the bare-tag match arm beyond True/False**
|
|
868
|
+
|
|
869
|
+
Replace lines 900-929 (`compile_variant_eq_arm`):
|
|
870
|
+
|
|
871
|
+
```rust
|
|
872
|
+
#[allow(clippy::too_many_arguments)]
|
|
873
|
+
fn compile_variant_eq_arm(
|
|
874
|
+
name: &str,
|
|
875
|
+
subject_vt: ValType,
|
|
876
|
+
scratch_local: u32,
|
|
877
|
+
case: &ast::Case,
|
|
878
|
+
rest: &[ast::Case],
|
|
879
|
+
body: &mut Vec<u8>,
|
|
880
|
+
ctx: &LocalCtx,
|
|
881
|
+
state: &mut ModuleState,
|
|
882
|
+
) -> Result<(), String> {
|
|
883
|
+
// Only Bool's own variants have a concrete runtime representation in v1.5.
|
|
884
|
+
let tag = match name {
|
|
885
|
+
"True" => 1i32,
|
|
886
|
+
"False" => 0i32,
|
|
887
|
+
other => return Err(format!("codegen: enum variant pattern '{}' is not yet supported (only True/False)", other)),
|
|
888
|
+
};
|
|
889
|
+
if subject_vt != ValType::I32 {
|
|
890
|
+
return Err("codegen: Bool match pattern against a non-Bool subject".to_string());
|
|
891
|
+
}
|
|
892
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
893
|
+
Instruction::I32Const(tag).encode(body);
|
|
894
|
+
Instruction::I32Eq.encode(body);
|
|
895
|
+
Instruction::If(BlockType::Empty).encode(body);
|
|
896
|
+
compile_block(&case.body, body, ctx, state)?;
|
|
897
|
+
Instruction::Else.encode(body);
|
|
898
|
+
compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
|
|
899
|
+
Instruction::End.encode(body);
|
|
900
|
+
Ok(())
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
with:
|
|
905
|
+
|
|
906
|
+
```rust
|
|
907
|
+
#[allow(clippy::too_many_arguments)]
|
|
908
|
+
fn compile_variant_eq_arm(
|
|
909
|
+
name: &str,
|
|
910
|
+
subject_vt: ValType,
|
|
911
|
+
scratch_local: u32,
|
|
912
|
+
case: &ast::Case,
|
|
913
|
+
rest: &[ast::Case],
|
|
914
|
+
body: &mut Vec<u8>,
|
|
915
|
+
ctx: &LocalCtx,
|
|
916
|
+
state: &mut ModuleState,
|
|
917
|
+
) -> Result<(), String> {
|
|
918
|
+
let info = ctx
|
|
919
|
+
.enum_variants
|
|
920
|
+
.get(name)
|
|
921
|
+
.ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
|
|
922
|
+
if subject_vt != ValType::I32 {
|
|
923
|
+
return Err(format!("codegen: enum tag pattern '{}' against a non-enum subject", name));
|
|
924
|
+
}
|
|
925
|
+
let tag = info.tag;
|
|
926
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
927
|
+
Instruction::I32Const(tag).encode(body);
|
|
928
|
+
Instruction::I32Eq.encode(body);
|
|
929
|
+
Instruction::If(BlockType::Empty).encode(body);
|
|
930
|
+
compile_block(&case.body, body, ctx, state)?;
|
|
931
|
+
Instruction::Else.encode(body);
|
|
932
|
+
compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
|
|
933
|
+
Instruction::End.encode(body);
|
|
934
|
+
Ok(())
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
- [ ] **Step 4: Implement the constructor-pattern match arm**
|
|
939
|
+
|
|
940
|
+
In `compile_match_arms`, replace line 896:
|
|
941
|
+
|
|
942
|
+
```rust
|
|
943
|
+
ast::CasePattern::Class { .. } => Err("codegen: constructor match patterns are not yet supported".to_string()),
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
with:
|
|
947
|
+
|
|
948
|
+
```rust
|
|
949
|
+
ast::CasePattern::Class { name, fields } => {
|
|
950
|
+
compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, case, rest, body, ctx, state)
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Then add this new function right after `compile_variant_eq_arm`:
|
|
955
|
+
|
|
956
|
+
```rust
|
|
957
|
+
#[allow(clippy::too_many_arguments)]
|
|
958
|
+
fn compile_variant_constructor_arm(
|
|
959
|
+
name: &str,
|
|
960
|
+
fields: &[ast::CasePattern],
|
|
961
|
+
subject_vt: ValType,
|
|
962
|
+
scratch_local: u32,
|
|
963
|
+
case: &ast::Case,
|
|
964
|
+
rest: &[ast::Case],
|
|
965
|
+
body: &mut Vec<u8>,
|
|
966
|
+
ctx: &LocalCtx,
|
|
967
|
+
state: &mut ModuleState,
|
|
968
|
+
) -> Result<(), String> {
|
|
969
|
+
let info = ctx
|
|
970
|
+
.enum_variants
|
|
971
|
+
.get(name)
|
|
972
|
+
.ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
|
|
973
|
+
if subject_vt != ValType::I32 {
|
|
974
|
+
return Err(format!("codegen: constructor pattern '{}' against a non-enum subject", name));
|
|
975
|
+
}
|
|
976
|
+
if fields.len() != info.field_types.len() {
|
|
977
|
+
return Err(format!(
|
|
978
|
+
"codegen: constructor pattern '{}' expects {} field(s), got {}",
|
|
979
|
+
name, info.field_types.len(), fields.len()
|
|
980
|
+
));
|
|
981
|
+
}
|
|
982
|
+
let tag = info.tag;
|
|
983
|
+
let field_types = info.field_types.clone();
|
|
984
|
+
|
|
985
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
986
|
+
Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
|
|
987
|
+
Instruction::I32Const(tag).encode(body);
|
|
988
|
+
Instruction::I32Eq.encode(body);
|
|
989
|
+
Instruction::If(BlockType::Empty).encode(body);
|
|
990
|
+
for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() {
|
|
991
|
+
let bind_name = match pat {
|
|
992
|
+
ast::CasePattern::Name(n) => Some(n.as_str()),
|
|
993
|
+
ast::CasePattern::Wildcard => None,
|
|
994
|
+
_ => return Err("codegen: only bare bindings or '_' are supported inside a constructor pattern".to_string()),
|
|
995
|
+
};
|
|
996
|
+
if let Some(n) = bind_name {
|
|
997
|
+
let idx = ctx
|
|
998
|
+
.locals
|
|
999
|
+
.get(n)
|
|
1000
|
+
.copied()
|
|
1001
|
+
.ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
|
|
1002
|
+
Instruction::LocalGet(scratch_local).encode(body);
|
|
1003
|
+
let offset = ((i + 1) as u64) * 8;
|
|
1004
|
+
match plum_type_to_valtype(field_ty) {
|
|
1005
|
+
ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }),
|
|
1006
|
+
ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }),
|
|
1007
|
+
_ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }),
|
|
1008
|
+
}.encode(body);
|
|
1009
|
+
Instruction::LocalSet(idx).encode(body);
|
|
1010
|
+
ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone()));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
compile_block(&case.body, body, ctx, state)?;
|
|
1014
|
+
Instruction::Else.encode(body);
|
|
1015
|
+
compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
|
|
1016
|
+
Instruction::End.encode(body);
|
|
1017
|
+
Ok(())
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
- [ ] **Step 5: Run codegen tests**
|
|
1022
|
+
|
|
1023
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
|
|
1024
|
+
Expected: all pass, including Task 3's `payload_variant_construction_compiles_and_runs` (returns 7) and `multi_field_variant_construction_compiles_and_runs` (returns 12).
|
|
1025
|
+
|
|
1026
|
+
- [ ] **Step 6: Add a dedicated test for a payload-free variant tag pattern beyond Bool, and one for `_`-wildcard inside a constructor pattern**
|
|
1027
|
+
|
|
1028
|
+
Append to `plum-wasm-codegen/tests/codegen_tests.rs`:
|
|
1029
|
+
|
|
1030
|
+
```rust
|
|
1031
|
+
#[test]
|
|
1032
|
+
fn non_bool_bare_tag_pattern_runs_correctly() {
|
|
1033
|
+
let src = "\
|
|
1034
|
+
enum Color =
|
|
1035
|
+
| Red
|
|
1036
|
+
| Green
|
|
1037
|
+
| Blue
|
|
1038
|
+
|
|
1039
|
+
code(c: Color) -> Int =
|
|
1040
|
+
match c
|
|
1041
|
+
Red =>
|
|
1042
|
+
return 1
|
|
1043
|
+
Green =>
|
|
1044
|
+
return 2
|
|
1045
|
+
Blue =>
|
|
1046
|
+
return 3
|
|
1047
|
+
|
|
1048
|
+
main() -> Int =
|
|
1049
|
+
code(Green)
|
|
1050
|
+
";
|
|
1051
|
+
let source = parse(src);
|
|
1052
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1053
|
+
assert_eq!(run_main(&bytes), 2);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
#[test]
|
|
1057
|
+
fn constructor_pattern_wildcard_field_runs_correctly() {
|
|
1058
|
+
let src = "\
|
|
1059
|
+
enum Option =
|
|
1060
|
+
| Some(Int)
|
|
1061
|
+
| None
|
|
1062
|
+
|
|
1063
|
+
isSome(o: Option) -> Int =
|
|
1064
|
+
match o
|
|
1065
|
+
Some(_) =>
|
|
1066
|
+
return 1
|
|
1067
|
+
None =>
|
|
1068
|
+
return 0
|
|
1069
|
+
|
|
1070
|
+
main() -> Int =
|
|
1071
|
+
isSome(Some(99))
|
|
1072
|
+
";
|
|
1073
|
+
let source = parse(src);
|
|
1074
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
1075
|
+
assert_eq!(run_main(&bytes), 1);
|
|
1076
|
+
}
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
|
|
1080
|
+
Expected: both pass.
|
|
1081
|
+
|
|
1082
|
+
- [ ] **Step 7: Run the full workspace test suite**
|
|
1083
|
+
|
|
1084
|
+
Run: `cargo test --workspace`
|
|
1085
|
+
Expected: all green, including `plum-wasm-codegen`'s `examples_test.rs` (see Task 5 — its `match_example_reports_clear_unsupported_pattern_errors` test will now fail because `match.plum` compiles successfully; that's expected and fixed in the next task, not this one).
|
|
1086
|
+
|
|
1087
|
+
- [ ] **Step 8: Commit**
|
|
1088
|
+
|
|
1089
|
+
```bash
|
|
1090
|
+
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
|
|
1091
|
+
git commit -m "feat(plum-wasm-codegen): compile general enum match patterns (tags + constructors)"
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
### Task 5: Examples, outdated test expectations, and docs
|
|
1097
|
+
|
|
1098
|
+
**Files:**
|
|
1099
|
+
- Modify: `plum-wasm-codegen/tests/examples_test.rs:69-78`
|
|
1100
|
+
- Modify: `examples/match.plum` (add a `main` exercising construction, so the example is executed, not just compiled)
|
|
1101
|
+
- Modify: `README.md` (Known gaps section)
|
|
1102
|
+
- Test: same files above
|
|
1103
|
+
|
|
1104
|
+
**Interfaces:**
|
|
1105
|
+
- Consumes: everything from Tasks 1-4.
|
|
1106
|
+
- Produces: nothing further — this is the final integration/documentation task.
|
|
1107
|
+
|
|
1108
|
+
- [ ] **Step 1: Fix the now-outdated "expect error" example test**
|
|
1109
|
+
|
|
1110
|
+
`match.plum` no longer fails to compile — the `match_example_reports_clear_unsupported_pattern_errors` test in `plum-wasm-codegen/tests/examples_test.rs` currently asserts it does. In `plum-wasm-codegen/tests/examples_test.rs`, replace lines 69-78:
|
|
1111
|
+
|
|
1112
|
+
```rust
|
|
1113
|
+
/// match.plum and strings.plum intentionally exercise syntax beyond what codegen
|
|
1114
|
+
/// currently lowers (non-Bool enum-tag/constructor match patterns, string
|
|
1115
|
+
/// interpolation) — they must fail loudly with a clear message, not silently
|
|
1116
|
+
/// produce wrong wasm.
|
|
1117
|
+
#[test]
|
|
1118
|
+
fn match_example_reports_clear_unsupported_pattern_errors() {
|
|
1119
|
+
let source = parse_file("match.plum");
|
|
1120
|
+
let err = compile_source(&source).expect_err("non-Bool enum-tag patterns are not yet supported");
|
|
1121
|
+
assert!(err.contains("enum variant pattern"), "got: {}", err);
|
|
1122
|
+
}
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
with:
|
|
1126
|
+
|
|
1127
|
+
```rust
|
|
1128
|
+
/// match.plum now exercises fully-supported syntax (general enum tag and
|
|
1129
|
+
/// constructor patterns) and must compile and run correctly end to end.
|
|
1130
|
+
#[test]
|
|
1131
|
+
fn match_example_compiles_and_runs_correctly() {
|
|
1132
|
+
let bytes = assert_compiles("match.plum");
|
|
1133
|
+
let engine = wasmtime::Engine::default();
|
|
1134
|
+
let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
|
|
1135
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
1136
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
|
|
1137
|
+
let main = instance
|
|
1138
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
1139
|
+
.expect("main should have signature () -> i64");
|
|
1140
|
+
let result = main.call(&mut store, ()).expect("main should not trap");
|
|
1141
|
+
// describeOption(Some(5)) = 5
|
|
1142
|
+
assert_eq!(result, 5);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/// strings.plum still exercises string interpolation, which remains unimplemented.
|
|
1146
|
+
#[test]
|
|
1147
|
+
fn strings_example_reports_clear_interpolation_error() {
|
|
1148
|
+
let source = parse_file("strings.plum");
|
|
1149
|
+
let err = compile_source(&source).expect_err("string interpolation is not yet supported");
|
|
1150
|
+
assert!(err.contains("interpolation"), "got: {}", err);
|
|
1151
|
+
}
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
Then delete the now-duplicate copy of the same test that already exists lower in the file (originally at lines 80-85, now shifted down by the edit above) — find and remove this exact block, leaving only the one just added above:
|
|
1155
|
+
|
|
1156
|
+
```rust
|
|
1157
|
+
#[test]
|
|
1158
|
+
fn strings_example_reports_clear_interpolation_error() {
|
|
1159
|
+
let source = parse_file("strings.plum");
|
|
1160
|
+
let err = compile_source(&source).expect_err("string interpolation is not yet supported");
|
|
1161
|
+
assert!(err.contains("interpolation"), "got: {}", err);
|
|
1162
|
+
}
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
(There must be exactly one `strings_example_reports_clear_interpolation_error` function left in the file — `cargo test` will fail to compile with a "duplicate definition" error if both remain.)
|
|
1166
|
+
|
|
1167
|
+
- [ ] **Step 2: Add a `main` to `examples/match.plum` so it's actually executed, not just compiled**
|
|
1168
|
+
|
|
1169
|
+
Append to `examples/match.plum`:
|
|
1170
|
+
|
|
1171
|
+
```plum
|
|
1172
|
+
|
|
1173
|
+
main() -> Int =
|
|
1174
|
+
describeOption(Some(5))
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
- [ ] **Step 3: Run codegen tests**
|
|
1178
|
+
|
|
1179
|
+
Run: `cargo test -p plum-wasm-codegen --test examples_test`
|
|
1180
|
+
Expected: `match_example_compiles_and_runs_correctly` passes (returns 5), `strings_example_reports_clear_interpolation_error` still passes.
|
|
1181
|
+
|
|
1182
|
+
- [ ] **Step 4: Update the README's Known Gaps section**
|
|
1183
|
+
|
|
1184
|
+
In `README.md`, find (around line 319-327):
|
|
1185
|
+
|
|
1186
|
+
```markdown
|
|
1187
|
+
### Known gaps
|
|
1188
|
+
|
|
1189
|
+
Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code:
|
|
1190
|
+
|
|
1191
|
+
- string interpolation (plain, non-interpolated string literals do compile)
|
|
1192
|
+
- `match` patterns other than integer literals, bindings, wildcard, and `True`/`False`; non-Bool enum-tag and constructor (`Some(v)`) patterns aren't lowered yet
|
|
1193
|
+
- multi-subject `match` (`match a, b`)
|
|
1194
|
+
- user-defined generics (they type-check but aren't monomorphized)
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
Replace with:
|
|
1198
|
+
|
|
1199
|
+
```markdown
|
|
1200
|
+
### Known gaps
|
|
1201
|
+
|
|
1202
|
+
Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code:
|
|
1203
|
+
|
|
1204
|
+
- string interpolation (plain, non-interpolated string literals do compile)
|
|
1205
|
+
- multi-subject `match` (`match a, b`)
|
|
1206
|
+
- user-defined generics (they type-check but aren't monomorphized) — this also blocks `libs/std`'s actual `Option`/`Result`/`List`/`Map`, which are declared generically
|
|
1207
|
+
- nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_`
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
Also update the `match` section's prose just above it (around line 304, "Multiple comma-separated subjects/patterns are accepted by the grammar but not yet lowered by codegen.") — check whether it still needs the caveat about non-Bool enum tags; if that sentence mentions the now-fixed gap, trim it to talk only about multi-subject match remaining unsupported.
|
|
1211
|
+
|
|
1212
|
+
- [ ] **Step 5: Run the full test suite one more time**
|
|
1213
|
+
|
|
1214
|
+
Run:
|
|
1215
|
+
```bash
|
|
1216
|
+
cargo test --workspace
|
|
1217
|
+
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
|
|
1218
|
+
```
|
|
1219
|
+
Expected: everything green.
|
|
1220
|
+
|
|
1221
|
+
- [ ] **Step 6: Commit**
|
|
1222
|
+
|
|
1223
|
+
```bash
|
|
1224
|
+
git add plum-wasm-codegen/tests/examples_test.rs examples/match.plum README.md
|
|
1225
|
+
git commit -m "docs+test: general enum support is complete; update known gaps and example"
|
|
1226
|
+
```
|