plum

#treesitter#compiler#wasm

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

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


2cee306Peter 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
+ ```