plum

#treesitter#compiler#wasm

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

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


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