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-20-tail-position-and-grammar-gaps.md
f1e33d7 1
# Tail-Position Value Propagation and Grammar Gap Fixes Implementation Plan
f1e33d7 2
f1e33d7 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.
f1e33d7 4
f1e33d7 5
**Goal:** Fix two pre-existing, unrelated-to-enums defects discovered during general enum support: (1) a tree-sitter-plum grammar limitation where a multi-line body's trailing statement can only be `$.primary_expression`, not a full `$.expression`; (2) a `plum-wasm-codegen` gap where a function's tail `match`/`if` (without explicit `return` in every arm) silently drops its value instead of returning it, producing wasm that fails validation.
f1e33d7 6
f1e33d7 7
**Architecture:** Fix 1 is a one-line grammar change (`$.primary_expression` → `$.expression` in `_statement`) plus corpus tests. Fix 2 threads an `Option<ValType>` "value position" parameter through the match/if compilation functions (`compile_match`, `compile_match_arms`, `compile_variant_eq_arm`, `compile_variant_constructor_arm`, and a new `compile_if`), with two new small recursive helpers (`compile_stmt_in_value_position`, `compile_block_in_value_position`) that decide, for a statement/block that must produce the function's return value, whether to recurse further (nested `if`/`match`), leave a bare expression's value on the stack, pass through a `return`/`todo` unchanged (both are stack-polymorphic in wasm), or emit a clear compile error for any other shape.
f1e33d7 8
f1e33d7 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.
f1e33d7 10
f1e33d7 11
## Global Constraints
f1e33d7 12
f1e33d7 13
- Out of scope: generics monomorphization, multi-subject `match`, nested constructor patterns, a full "does every path return a value" static analysis in `plum-checker` — only make value-position `if`/`match` either compile correctly or fail with a clear `codegen:`-prefixed error, matching this file's existing error-message convention.
f1e33d7 14
- No changes needed in `plum-checker` for either fix — re-run its full suite (including `examples_test.rs`) to confirm no regression, but don't touch its source.
f1e33d7 15
- Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use the existing `"codegen: ..."` prefix convention already used throughout `plum-wasm-codegen/src/lib.rs`.
f1e33d7 16
- Every task must leave `cargo test --workspace` and `npx --yes tree-sitter-cli test` (from `tooling/tree-sitter-plum/`) green before moving to the next task.
f1e33d7 17
f1e33d7 18
---
f1e33d7 19
f1e33d7 20
### Task 1: Grammar — accept a full expression as a body's trailing statement
f1e33d7 21
f1e33d7 22
**Files:**
f1e33d7 23
- Modify: `tooling/tree-sitter-plum/grammar.js:166-179` (`_statement` rule)
f1e33d7 24
- Test: `tooling/tree-sitter-plum/test/corpus/` (new cases)
f1e33d7 25
f1e33d7 26
**Interfaces:**
f1e33d7 27
- Consumes: nothing from other tasks.
f1e33d7 28
- Produces: `_statement` accepts any `$.expression` (comparison, boolean-op, ternary, or the existing `$.primary_expression` alternatives), not just `$.primary_expression`. `plum-core`'s parser needs no change — `parse_case`/block-statement parsing already dispatches on node kind, and every new node kind reachable through `expression` (`comparison_operator`, `boolean_operator`, `ternary_expression`) is already handled by `AstParser::parse_expression` (used for expression-context nodes elsewhere).
f1e33d7 29
f1e33d7 30
- [ ] **Step 1: Make the grammar change**
f1e33d7 31
f1e33d7 32
In `tooling/tree-sitter-plum/grammar.js`, change:
f1e33d7 33
f1e33d7 34
```js
f1e33d7 35
    _statement: ($) =>
f1e33d7 36
      choice(
f1e33d7 37
        $.assign,
f1e33d7 38
        $.break,
f1e33d7 39
        $.continue,
f1e33d7 40
        $.assert,
f1e33d7 41
        $.for,
f1e33d7 42
        $.while,
f1e33d7 43
        $.if,
f1e33d7 44
        $.match,
f1e33d7 45
        $.return,
f1e33d7 46
        $.todo,
f1e33d7 47
        $.primary_expression
f1e33d7 48
      ),
f1e33d7 49
```
f1e33d7 50
f1e33d7 51
to:
f1e33d7 52
f1e33d7 53
```js
f1e33d7 54
    _statement: ($) =>
f1e33d7 55
      choice(
f1e33d7 56
        $.assign,
f1e33d7 57
        $.break,
f1e33d7 58
        $.continue,
f1e33d7 59
        $.assert,
f1e33d7 60
        $.for,
f1e33d7 61
        $.while,
f1e33d7 62
        $.if,
f1e33d7 63
        $.match,
f1e33d7 64
        $.return,
f1e33d7 65
        $.todo,
f1e33d7 66
        $.expression
f1e33d7 67
      ),
f1e33d7 68
```
f1e33d7 69
f1e33d7 70
- [ ] **Step 2: Regenerate and run the existing corpus suite**
f1e33d7 71
f1e33d7 72
```bash
f1e33d7 73
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate && npx --yes tree-sitter-cli test
f1e33d7 74
```
f1e33d7 75
f1e33d7 76
Expected: generation succeeds with no unresolved-conflict errors, and all pre-existing corpus cases still pass (every statement previously reachable via `primary_expression` remains reachable, since `expression`'s own last alternative is `primary_expression` — see `grammar.js`'s `expression` rule).
f1e33d7 77
f1e33d7 78
- [ ] **Step 3: Add corpus cases for the newly-parseable statement forms**
f1e33d7 79
f1e33d7 80
Append to `tooling/tree-sitter-plum/test/corpus/function.txt` three new cases (input half only — the next step fills in the expected tree):
f1e33d7 81
f1e33d7 82
```
f1e33d7 83
================================================================================
f1e33d7 84
function - bare comparison as body's trailing statement
f1e33d7 85
================================================================================
f1e33d7 86
f1e33d7 87
isNone(o: Option) -> Bool =
f1e33d7 88
  o == None
f1e33d7 89
f1e33d7 90
--------------------------------------------------------------------------------
f1e33d7 91
================================================================================
f1e33d7 92
function - bare boolean-operator as body's trailing statement
f1e33d7 93
================================================================================
f1e33d7 94
f1e33d7 95
bothTrue(a: Bool, b: Bool) -> Bool =
f1e33d7 96
  a && b
f1e33d7 97
f1e33d7 98
--------------------------------------------------------------------------------
f1e33d7 99
================================================================================
f1e33d7 100
function - bare ternary as body's trailing statement
f1e33d7 101
================================================================================
f1e33d7 102
f1e33d7 103
pick(cond: Bool, a: Int, b: Int) -> Int =
f1e33d7 104
  cond ? a : b
f1e33d7 105
f1e33d7 106
--------------------------------------------------------------------------------
f1e33d7 107
```
f1e33d7 108
f1e33d7 109
- [ ] **Step 4: Generate the expected trees and verify them**
f1e33d7 110
f1e33d7 111
```bash
f1e33d7 112
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test -u -f "bare comparison as body" && npx --yes tree-sitter-cli test -u -f "bare boolean-operator as body" && npx --yes tree-sitter-cli test -u -f "bare ternary as body"
f1e33d7 113
```
f1e33d7 114
f1e33d7 115
Open `test/corpus/function.txt` and confirm each of the three new cases' generated tree has **no** `ERROR`/`MISSING` node — the comparison/boolean-op/ternary node must appear as a single, complete node directly inside the function's `body`, not split into two separate statements.
f1e33d7 116
f1e33d7 117
- [ ] **Step 5: Run the full corpus suite**
f1e33d7 118
f1e33d7 119
```bash
f1e33d7 120
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
f1e33d7 121
```
f1e33d7 122
f1e33d7 123
Expected: all cases pass, old and new.
f1e33d7 124
f1e33d7 125
- [ ] **Step 6: Run the full Rust workspace suite**
f1e33d7 126
f1e33d7 127
```bash
f1e33d7 128
cargo test --workspace
f1e33d7 129
```
f1e33d7 130
f1e33d7 131
Expected: green (the grammar change doesn't remove any previously-valid parse, so nothing downstream should regress; `plum-checker`/`plum-wasm-codegen` tests exercise the parser transitively via their own `parse()` helpers).
f1e33d7 132
f1e33d7 133
- [ ] **Step 7: Commit**
f1e33d7 134
f1e33d7 135
```bash
f1e33d7 136
git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/test/corpus/function.txt
f1e33d7 137
git add tooling/tree-sitter-plum/src  # generated parser.c etc, only if tracked — check `git status` first
f1e33d7 138
git commit -m "fix(tree-sitter-plum): allow a full expression as a body's trailing statement"
f1e33d7 139
```
f1e33d7 140
f1e33d7 141
---
f1e33d7 142
f1e33d7 143
### Task 2: Codegen — recursive value-position propagation for tail `if`/`match`
f1e33d7 144
f1e33d7 145
**Files:**
f1e33d7 146
- Modify: `plum-wasm-codegen/src/lib.rs` (see exact line ranges in each step below — line numbers assume Task 1 has already landed and did not touch this file, so they should still be accurate; verify by reading the current file before editing)
f1e33d7 147
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
f1e33d7 148
f1e33d7 149
**Interfaces:**
f1e33d7 150
- Consumes: nothing new from Task 1 (Task 1 only touched the grammar; this task's AST shapes — `ast::Stmt::If`, `ast::Stmt::Match`, `ast::Stmt::Expr`, `ast::Stmt::Return`, `ast::Stmt::Todo` — are unchanged).
f1e33d7 151
- Produces: `compile_block_as_fn_body`'s signature changes from `(..., has_return_value: bool)` to `(..., result_vt: Option<ValType>)` — its one call site (in `compile_fn_body`) is part of this task. `compile_match`'s signature gains a trailing `result_vt: Option<ValType>` parameter; so do `compile_match_arms`, `compile_variant_eq_arm`, `compile_variant_constructor_arm`. Three new functions: `block_type_for(Option<ValType>) -> BlockType`, `compile_case_body(&ast::Block, Option<ValType>, ...) -> Result<(), String>`, `compile_if(&ast::If, Option<ValType>, ...) -> Result<(), String>`, `compile_block_in_value_position(&ast::Block, ValType, ...) -> Result<(), String>`, `compile_stmt_in_value_position(&ast::Stmt, ValType, ...) -> Result<(), String>`. Task 3 does not depend on any of these names directly — it only exercises the feature through `.plum` source and `compile_source`.
f1e33d7 152
f1e33d7 153
- [ ] **Step 1: Write failing tests**
f1e33d7 154
f1e33d7 155
Append to `plum-wasm-codegen/tests/codegen_tests.rs`:
f1e33d7 156
f1e33d7 157
```rust
f1e33d7 158
#[test]
f1e33d7 159
fn tail_match_without_return_runs_correctly() {
f1e33d7 160
    let src = "\
f1e33d7 161
bindExample(n: Int) -> Int =
f1e33d7 162
  match n
f1e33d7 163
    x =>
f1e33d7 164
      x
f1e33d7 165
f1e33d7 166
main() -> Int =
f1e33d7 167
  bindExample(5)
f1e33d7 168
";
f1e33d7 169
    let source = parse(src);
f1e33d7 170
    let bytes = compile_source(&source).expect("compile failed");
f1e33d7 171
    assert_eq!(run_main(&bytes), 5);
f1e33d7 172
}
f1e33d7 173
f1e33d7 174
#[test]
f1e33d7 175
fn tail_if_without_return_runs_correctly() {
f1e33d7 176
    let src = "\
f1e33d7 177
abs(n: Int) -> Int =
f1e33d7 178
  if n < 0
f1e33d7 179
    -n
f1e33d7 180
  else
f1e33d7 181
    n
f1e33d7 182
f1e33d7 183
main() -> Int =
f1e33d7 184
  abs(-7)
f1e33d7 185
";
f1e33d7 186
    let source = parse(src);
f1e33d7 187
    let bytes = compile_source(&source).expect("compile failed");
f1e33d7 188
    assert_eq!(run_main(&bytes), 7);
f1e33d7 189
}
f1e33d7 190
f1e33d7 191
#[test]
f1e33d7 192
fn tail_if_nested_inside_match_arm_without_return_runs_correctly() {
f1e33d7 193
    let src = "\
f1e33d7 194
classify(n: Int) -> Int =
f1e33d7 195
  match n
f1e33d7 196
    0 =>
f1e33d7 197
      1
f1e33d7 198
    x =>
f1e33d7 199
      if x < 0
f1e33d7 200
        -1
f1e33d7 201
      else
f1e33d7 202
        2
f1e33d7 203
f1e33d7 204
main() -> Int =
f1e33d7 205
  classify(-5)
f1e33d7 206
";
f1e33d7 207
    let source = parse(src);
f1e33d7 208
    let bytes = compile_source(&source).expect("compile failed");
f1e33d7 209
    assert_eq!(run_main(&bytes), -1);
f1e33d7 210
}
f1e33d7 211
f1e33d7 212
#[test]
f1e33d7 213
fn tail_match_mixing_return_and_bare_expr_arms_runs_correctly() {
f1e33d7 214
    let src = "\
f1e33d7 215
describe(n: Int) -> Int =
f1e33d7 216
  match n
f1e33d7 217
    0 =>
f1e33d7 218
      return 100
f1e33d7 219
    x =>
f1e33d7 220
      x * 2
f1e33d7 221
f1e33d7 222
main() -> Int =
f1e33d7 223
  describe(21)
f1e33d7 224
";
f1e33d7 225
    let source = parse(src);
f1e33d7 226
    let bytes = compile_source(&source).expect("compile failed");
f1e33d7 227
    assert_eq!(run_main(&bytes), 42);
f1e33d7 228
}
f1e33d7 229
f1e33d7 230
#[test]
f1e33d7 231
fn tail_enum_match_without_return_runs_correctly() {
f1e33d7 232
    let src = "\
f1e33d7 233
enum Option =
f1e33d7 234
  | Some(Int)
f1e33d7 235
  | None
f1e33d7 236
f1e33d7 237
unwrapOr(o: Option, default: Int) -> Int =
f1e33d7 238
  match o
f1e33d7 239
    Some(v) =>
f1e33d7 240
      v
f1e33d7 241
    None =>
f1e33d7 242
      default
f1e33d7 243
f1e33d7 244
main() -> Int =
f1e33d7 245
  unwrapOr(Some(9), 0)
f1e33d7 246
";
f1e33d7 247
    let source = parse(src);
f1e33d7 248
    let bytes = compile_source(&source).expect("compile failed");
f1e33d7 249
    assert_eq!(run_main(&bytes), 9);
f1e33d7 250
}
f1e33d7 251
f1e33d7 252
#[test]
f1e33d7 253
fn tail_if_without_else_is_a_clear_error() {
f1e33d7 254
    let src = "\
f1e33d7 255
bad(n: Int) -> Int =
f1e33d7 256
  if n < 0
f1e33d7 257
    return 1
f1e33d7 258
";
f1e33d7 259
    let source = parse(src);
f1e33d7 260
    let err = compile_source(&source).expect_err("if without else in value position must be a clear error, not invalid wasm");
f1e33d7 261
    assert!(err.contains("doesn't produce a return value"), "got: {}", err);
f1e33d7 262
}
f1e33d7 263
f1e33d7 264
#[test]
f1e33d7 265
fn tail_match_non_exhaustive_is_a_clear_error() {
f1e33d7 266
    let src = "\
f1e33d7 267
bad(n: Int) -> Int =
f1e33d7 268
  match n
f1e33d7 269
    0 =>
f1e33d7 270
      1
f1e33d7 271
";
f1e33d7 272
    let source = parse(src);
f1e33d7 273
    let err = compile_source(&source).expect_err("non-exhaustive match in value position must be a clear error, not invalid wasm");
f1e33d7 274
    assert!(err.contains("doesn't produce a return value"), "got: {}", err);
f1e33d7 275
}
f1e33d7 276
f1e33d7 277
#[test]
f1e33d7 278
fn tail_match_arm_ending_in_non_value_statement_is_a_clear_error() {
f1e33d7 279
    let src = "\
f1e33d7 280
bad(n: Int) -> Int =
f1e33d7 281
  match n
f1e33d7 282
    x =>
f1e33d7 283
      y = x
f1e33d7 284
";
f1e33d7 285
    let source = parse(src);
f1e33d7 286
    let err = compile_source(&source).expect_err("a match arm ending in a non-value statement must be a clear error, not invalid wasm");
f1e33d7 287
    assert!(err.contains("doesn't produce a return value"), "got: {}", err);
f1e33d7 288
}
f1e33d7 289
```
f1e33d7 290
f1e33d7 291
- [ ] **Step 2: Run to see them fail**
f1e33d7 292
f1e33d7 293
Run: `cargo test -p plum-wasm-codegen --test codegen_tests tail_`
f1e33d7 294
Expected: the first five tests fail with `wasmparser::validate` errors surfacing as `compile failed` panics (the value is dropped, producing invalid wasm) or wrong runtime results; the last two "clear error" tests fail because `compile_source` currently returns `Ok` (or panics) instead of the expected `Err`.
f1e33d7 295
f1e33d7 296
- [ ] **Step 3: Read the current file to confirm line numbers, then make the edits**
f1e33d7 297
f1e33d7 298
Read `plum-wasm-codegen/src/lib.rs` around the ranges below before editing — Task 1 doesn't touch this file, so these should still be accurate, but verify.
f1e33d7 299
f1e33d7 300
**3a. Add `block_type_for` near the other small helpers** (e.g. right after `plum_type_to_valtype`, around line 249-256):
f1e33d7 301
f1e33d7 302
```rust
f1e33d7 303
fn block_type_for(result_vt: Option<ValType>) -> BlockType {
f1e33d7 304
    result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty)
f1e33d7 305
}
f1e33d7 306
```
f1e33d7 307
f1e33d7 308
**3b. Remove `stmt_always_diverges` and `block_always_diverges`** (currently lines 642-662) — they become dead code once `compile_block_as_fn_body` no longer needs them (Step 3d). Delete this whole block:
f1e33d7 309
f1e33d7 310
```rust
f1e33d7 311
/// True if control can never fall through past this statement — every reachable path
f1e33d7 312
/// ends in a `return`. Used to decide whether a tail-position If/Match needs a
f1e33d7 313
/// trailing `unreachable` to satisfy wasm's per-block (not whole-function) validation
f1e33d7 314
/// when the function declares a non-Unit return type.
f1e33d7 315
fn stmt_always_diverges(stmt: &ast::Stmt) -> bool {
f1e33d7 316
    match stmt {
f1e33d7 317
        ast::Stmt::Return(_) | ast::Stmt::Todo => true,
f1e33d7 318
        ast::Stmt::If(if_) => {
f1e33d7 319
            if_.else_.is_some()
f1e33d7 320
                && block_always_diverges(&if_.body)
f1e33d7 321
                && if_.else_ifs.iter().all(|ei| block_always_diverges(&ei.body))
f1e33d7 322
                && if_.else_.as_ref().is_some_and(block_always_diverges)
f1e33d7 323
        }
f1e33d7 324
        ast::Stmt::Match(m) => !m.cases.is_empty() && m.cases.iter().all(|c| block_always_diverges(&c.body)),
f1e33d7 325
        _ => false,
f1e33d7 326
    }
f1e33d7 327
}
f1e33d7 328
f1e33d7 329
fn block_always_diverges(block: &ast::Block) -> bool {
f1e33d7 330
    block.stmts.last().map(stmt_always_diverges).unwrap_or(false)
f1e33d7 331
}
f1e33d7 332
```
f1e33d7 333
f1e33d7 334
**3c. Add the new value-position helpers**, right after `compile_block` (currently lines 635-640) and before where `stmt_always_diverges` used to be:
f1e33d7 335
f1e33d7 336
```rust
f1e33d7 337
/// Compiles a case/branch body either as an ordinary statement block (`result_vt: None`)
f1e33d7 338
/// or, when in value position, via `compile_block_in_value_position` so its own tail
f1e33d7 339
/// statement propagates a value instead of being dropped.
f1e33d7 340
fn compile_case_body(
f1e33d7 341
    block: &ast::Block,
f1e33d7 342
    result_vt: Option<ValType>,
f1e33d7 343
    body: &mut Vec<u8>,
f1e33d7 344
    ctx: &LocalCtx,
f1e33d7 345
    state: &mut ModuleState,
f1e33d7 346
) -> Result<(), String> {
f1e33d7 347
    match result_vt {
f1e33d7 348
        Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state),
f1e33d7 349
        None => compile_block(block, body, ctx, state),
f1e33d7 350
    }
f1e33d7 351
}
f1e33d7 352
f1e33d7 353
/// Compiles a block whose value must be produced when control reaches its end — every
f1e33d7 354
/// statement except the last compiles normally; the last is compiled via
f1e33d7 355
/// `compile_stmt_in_value_position`.
f1e33d7 356
fn compile_block_in_value_position(
f1e33d7 357
    block: &ast::Block,
f1e33d7 358
    result_vt: ValType,
f1e33d7 359
    body: &mut Vec<u8>,
f1e33d7 360
    ctx: &LocalCtx,
f1e33d7 361
    state: &mut ModuleState,
f1e33d7 362
) -> Result<(), String> {
f1e33d7 363
    let (last, rest) = block.stmts.split_last().ok_or_else(|| {
f1e33d7 364
        "codegen: function has a control-flow path that doesn't produce a return value (empty branch)".to_string()
f1e33d7 365
    })?;
f1e33d7 366
    for stmt in rest {
f1e33d7 367
        compile_stmt(stmt, body, ctx, state)?;
f1e33d7 368
    }
f1e33d7 369
    compile_stmt_in_value_position(last, result_vt, body, ctx, state)
f1e33d7 370
}
f1e33d7 371
f1e33d7 372
/// Compiles a single statement in value position: a bare expression is left on the stack
f1e33d7 373
/// (not dropped); `return`/`todo` compile normally (both are stack-polymorphic in wasm —
f1e33d7 374
/// control never falls through past them, so no value is needed on this path); `if`/`match`
f1e33d7 375
/// recurse so every arm/branch resolves the same way. Any other statement kind can't
f1e33d7 376
/// produce a value, so this returns a clear error instead of ever emitting wasm that
f1e33d7 377
/// would fail validation.
f1e33d7 378
fn compile_stmt_in_value_position(
f1e33d7 379
    stmt: &ast::Stmt,
f1e33d7 380
    result_vt: ValType,
f1e33d7 381
    body: &mut Vec<u8>,
f1e33d7 382
    ctx: &LocalCtx,
f1e33d7 383
    state: &mut ModuleState,
f1e33d7 384
) -> Result<(), String> {
f1e33d7 385
    match stmt {
f1e33d7 386
        ast::Stmt::Expr(e) => compile_expr(e, body, ctx, state),
f1e33d7 387
        ast::Stmt::Return(_) | ast::Stmt::Todo => compile_stmt(stmt, body, ctx, state),
f1e33d7 388
        ast::Stmt::If(if_) => compile_if(if_, Some(result_vt), body, ctx, state),
f1e33d7 389
        ast::Stmt::Match(m) => compile_match(m, body, ctx, state, Some(result_vt)),
f1e33d7 390
        _ => Err(
f1e33d7 391
            "codegen: function has a control-flow path that doesn't produce a return value".to_string(),
f1e33d7 392
        ),
f1e33d7 393
    }
f1e33d7 394
}
f1e33d7 395
f1e33d7 396
/// Compiles an `if`/`else if`/`else` chain. `result_vt` is `None` for an ordinary statement
f1e33d7 397
/// (each branch is `BlockType::Empty`, nothing left on the stack) or `Some(vt)` when this
f1e33d7 398
/// `if` is in value position — every branch must then leave a `vt` value on the stack, which
f1e33d7 399
/// requires an `else` (a value can't be produced on a path that doesn't exist).
f1e33d7 400
fn compile_if(
f1e33d7 401
    if_: &ast::If,
f1e33d7 402
    result_vt: Option<ValType>,
f1e33d7 403
    body: &mut Vec<u8>,
f1e33d7 404
    ctx: &LocalCtx,
f1e33d7 405
    state: &mut ModuleState,
f1e33d7 406
) -> Result<(), String> {
f1e33d7 407
    if result_vt.is_some() && if_.else_.is_none() {
f1e33d7 408
        return Err(
f1e33d7 409
            "codegen: function has a control-flow path that doesn't produce a return value (if without else)".to_string(),
f1e33d7 410
        );
f1e33d7 411
    }
f1e33d7 412
    let bt = block_type_for(result_vt);
f1e33d7 413
    compile_expr(&if_.condition, body, ctx, state)?;
f1e33d7 414
    Instruction::If(bt).encode(body);
f1e33d7 415
    compile_case_body(&if_.body, result_vt, body, ctx, state)?;
f1e33d7 416
    if !if_.else_ifs.is_empty() || if_.else_.is_some() {
f1e33d7 417
        Instruction::Else.encode(body);
f1e33d7 418
        for ei in &if_.else_ifs {
f1e33d7 419
            compile_expr(&ei.condition, body, ctx, state)?;
f1e33d7 420
            Instruction::If(bt).encode(body);
f1e33d7 421
            compile_case_body(&ei.body, result_vt, body, ctx, state)?;
f1e33d7 422
            Instruction::Else.encode(body);
f1e33d7 423
        }
f1e33d7 424
        if let Some(else_block) = &if_.else_ {
f1e33d7 425
            compile_case_body(else_block, result_vt, body, ctx, state)?;
f1e33d7 426
        }
f1e33d7 427
        for _ in &if_.else_ifs {
f1e33d7 428
            Instruction::End.encode(body);
f1e33d7 429
        }
f1e33d7 430
    }
f1e33d7 431
    Instruction::End.encode(body);
f1e33d7 432
    Ok(())
f1e33d7 433
}
f1e33d7 434
```
f1e33d7 435
f1e33d7 436
**3d. Simplify `compile_block_as_fn_body`** (currently lines 664-706) — replace the whole function:
f1e33d7 437
f1e33d7 438
```rust
f1e33d7 439
/// Compiles a block that is the body of a function. If the function returns a value,
f1e33d7 440
/// its tail statement is compiled in value position (see `compile_stmt_in_value_position`)
f1e33d7 441
/// so a bare expression, or an `if`/`match` whose arms resolve to one, propagates that
f1e33d7 442
/// value instead of being dropped.
f1e33d7 443
fn compile_block_as_fn_body(
f1e33d7 444
    block: &ast::Block,
f1e33d7 445
    body: &mut Vec<u8>,
f1e33d7 446
    ctx: &LocalCtx,
f1e33d7 447
    state: &mut ModuleState,
f1e33d7 448
    result_vt: Option<ValType>,
f1e33d7 449
) -> Result<(), String> {
f1e33d7 450
    match result_vt {
f1e33d7 451
        Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state),
f1e33d7 452
        None => compile_block(block, body, ctx, state),
f1e33d7 453
    }
f1e33d7 454
}
f1e33d7 455
```
f1e33d7 456
f1e33d7 457
**3e. Update `compile_fn_body`'s call site** (currently around lines 620 and 627):
f1e33d7 458
f1e33d7 459
Replace:
f1e33d7 460
f1e33d7 461
```rust
f1e33d7 462
    let has_return_value = f.returns.as_ref().map(|r| r.name != "Unit").unwrap_or(false);
f1e33d7 463
f1e33d7 464
    match &f.body {
f1e33d7 465
        ast::FnBody::Expr(e) => {
f1e33d7 466
            compile_expr(e, &mut body, &local_ctx, state)?;
f1e33d7 467
        }
f1e33d7 468
        ast::FnBody::Block(block) => {
f1e33d7 469
            compile_block_as_fn_body(block, &mut body, &local_ctx, state, has_return_value)?;
f1e33d7 470
        }
f1e33d7 471
    }
f1e33d7 472
```
f1e33d7 473
f1e33d7 474
with:
f1e33d7 475
f1e33d7 476
```rust
f1e33d7 477
    let result_vt = ret_type_to_wasm(f.returns.as_ref());
f1e33d7 478
f1e33d7 479
    match &f.body {
f1e33d7 480
        ast::FnBody::Expr(e) => {
f1e33d7 481
            compile_expr(e, &mut body, &local_ctx, state)?;
f1e33d7 482
        }
f1e33d7 483
        ast::FnBody::Block(block) => {
f1e33d7 484
            compile_block_as_fn_body(block, &mut body, &local_ctx, state, result_vt)?;
f1e33d7 485
        }
f1e33d7 486
    }
f1e33d7 487
```
f1e33d7 488
f1e33d7 489
**3f. Replace `compile_stmt`'s inline `If` arm** (currently lines 730-750) with a call to the new `compile_if`:
f1e33d7 490
f1e33d7 491
Replace:
f1e33d7 492
f1e33d7 493
```rust
f1e33d7 494
        ast::Stmt::If(if_) => {
f1e33d7 495
            compile_expr(&if_.condition, body, ctx, state)?;
f1e33d7 496
            Instruction::If(BlockType::Empty).encode(body);
f1e33d7 497
            compile_block(&if_.body, body, ctx, state)?;
f1e33d7 498
            if !if_.else_ifs.is_empty() || if_.else_.is_some() {
f1e33d7 499
                Instruction::Else.encode(body);
f1e33d7 500
                for ei in &if_.else_ifs {
f1e33d7 501
                    compile_expr(&ei.condition, body, ctx, state)?;
f1e33d7 502
                    Instruction::If(BlockType::Empty).encode(body);
f1e33d7 503
                    compile_block(&ei.body, body, ctx, state)?;
f1e33d7 504
                    Instruction::Else.encode(body);
f1e33d7 505
                }
f1e33d7 506
                if let Some(else_block) = &if_.else_ {
f1e33d7 507
                    compile_block(else_block, body, ctx, state)?;
f1e33d7 508
                }
f1e33d7 509
                for _ in &if_.else_ifs {
f1e33d7 510
                    Instruction::End.encode(body);
f1e33d7 511
                }
f1e33d7 512
            }
f1e33d7 513
            Instruction::End.encode(body);
f1e33d7 514
        }
f1e33d7 515
```
f1e33d7 516
f1e33d7 517
with:
f1e33d7 518
f1e33d7 519
```rust
f1e33d7 520
        ast::Stmt::If(if_) => {
f1e33d7 521
            compile_if(if_, None, body, ctx, state)?;
f1e33d7 522
        }
f1e33d7 523
```
f1e33d7 524
f1e33d7 525
**3g. Update `compile_stmt`'s `Match` arm** (currently `compile_match(m, body, ctx, state)?;`) to pass `None`:
f1e33d7 526
f1e33d7 527
```rust
f1e33d7 528
        ast::Stmt::Match(m) => {
f1e33d7 529
            compile_match(m, body, ctx, state, None)?;
f1e33d7 530
        }
f1e33d7 531
```
f1e33d7 532
f1e33d7 533
**3h. Update `compile_match`'s signature and body** (currently lines 844-863):
f1e33d7 534
f1e33d7 535
Replace:
f1e33d7 536
f1e33d7 537
```rust
f1e33d7 538
fn compile_match(m: &ast::Match, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
f1e33d7 539
    if m.subjects.len() != 1 {
f1e33d7 540
        return Err("codegen: multi-subject match is not yet supported".to_string());
f1e33d7 541
    }
f1e33d7 542
    let subject = &m.subjects[0];
f1e33d7 543
    let subject_ty = infer_local_type(subject, ctx);
f1e33d7 544
    let subject_vt = plum_type_to_valtype(&subject_ty);
f1e33d7 545
f1e33d7 546
    let key = m as *const ast::Match as usize;
f1e33d7 547
    let slot = *ctx
f1e33d7 548
        .match_scratch_index
f1e33d7 549
        .get(&key)
f1e33d7 550
        .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?;
f1e33d7 551
    let scratch_local = ctx.match_scratch_base + slot;
f1e33d7 552
f1e33d7 553
    compile_expr(subject, body, ctx, state)?;
f1e33d7 554
    Instruction::LocalSet(scratch_local).encode(body);
f1e33d7 555
f1e33d7 556
    compile_match_arms(&m.cases, subject_vt, scratch_local, body, ctx, state)
f1e33d7 557
}
f1e33d7 558
```
f1e33d7 559
f1e33d7 560
with:
f1e33d7 561
f1e33d7 562
```rust
f1e33d7 563
fn compile_match(
f1e33d7 564
    m: &ast::Match,
f1e33d7 565
    body: &mut Vec<u8>,
f1e33d7 566
    ctx: &LocalCtx,
f1e33d7 567
    state: &mut ModuleState,
f1e33d7 568
    result_vt: Option<ValType>,
f1e33d7 569
) -> Result<(), String> {
f1e33d7 570
    if m.subjects.len() != 1 {
f1e33d7 571
        return Err("codegen: multi-subject match is not yet supported".to_string());
f1e33d7 572
    }
f1e33d7 573
    let subject = &m.subjects[0];
f1e33d7 574
    let subject_ty = infer_local_type(subject, ctx);
f1e33d7 575
    let subject_vt = plum_type_to_valtype(&subject_ty);
f1e33d7 576
f1e33d7 577
    let key = m as *const ast::Match as usize;
f1e33d7 578
    let slot = *ctx
f1e33d7 579
        .match_scratch_index
f1e33d7 580
        .get(&key)
f1e33d7 581
        .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?;
f1e33d7 582
    let scratch_local = ctx.match_scratch_base + slot;
f1e33d7 583
f1e33d7 584
    compile_expr(subject, body, ctx, state)?;
f1e33d7 585
    Instruction::LocalSet(scratch_local).encode(body);
f1e33d7 586
f1e33d7 587
    compile_match_arms(&m.cases, subject_vt, scratch_local, result_vt, body, ctx, state)
f1e33d7 588
}
f1e33d7 589
```
f1e33d7 590
f1e33d7 591
**3i. Update `compile_match_arms`** (currently lines 865-921):
f1e33d7 592
f1e33d7 593
Replace the whole function:
f1e33d7 594
f1e33d7 595
```rust
f1e33d7 596
fn compile_match_arms(
f1e33d7 597
    cases: &[ast::Case],
f1e33d7 598
    subject_vt: ValType,
f1e33d7 599
    scratch_local: u32,
f1e33d7 600
    result_vt: Option<ValType>,
f1e33d7 601
    body: &mut Vec<u8>,
f1e33d7 602
    ctx: &LocalCtx,
f1e33d7 603
    state: &mut ModuleState,
f1e33d7 604
) -> Result<(), String> {
f1e33d7 605
    let (case, rest) = match cases.split_first() {
f1e33d7 606
        None => {
f1e33d7 607
            return match result_vt {
f1e33d7 608
                Some(_) => Err(
f1e33d7 609
                    "codegen: function has a control-flow path that doesn't produce a return value (non-exhaustive match)".to_string(),
f1e33d7 610
                ),
f1e33d7 611
                None => Ok(()),
f1e33d7 612
            };
f1e33d7 613
        }
f1e33d7 614
        Some(pair) => pair,
f1e33d7 615
    };
f1e33d7 616
    let pat = case.patterns.first().ok_or_else(|| "codegen: match case has no pattern".to_string())?;
f1e33d7 617
    match pat {
f1e33d7 618
        ast::CasePattern::Wildcard => {
f1e33d7 619
            // Any cases after a wildcard are unreachable, matching real match semantics.
f1e33d7 620
            compile_case_body(&case.body, result_vt, body, ctx, state)
f1e33d7 621
        }
f1e33d7 622
        ast::CasePattern::Name(n) => {
f1e33d7 623
            let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
f1e33d7 624
                && ctx.enum_variants.contains_key(n);
f1e33d7 625
            if is_variant {
f1e33d7 626
                compile_variant_eq_arm(n, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state)
f1e33d7 627
            } else {
f1e33d7 628
                let idx = ctx
f1e33d7 629
                    .locals
f1e33d7 630
                    .get(n)
f1e33d7 631
                    .copied()
f1e33d7 632
                    .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
f1e33d7 633
                Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 634
                Instruction::LocalSet(idx).encode(body);
f1e33d7 635
                ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plum_type_from_valtype_hint(subject_vt)));
f1e33d7 636
                compile_case_body(&case.body, result_vt, body, ctx, state)
f1e33d7 637
                // A binding arm always matches — any following cases are unreachable.
f1e33d7 638
            }
f1e33d7 639
        }
f1e33d7 640
        ast::CasePattern::Int(n) => {
f1e33d7 641
            if subject_vt != ValType::I64 {
f1e33d7 642
                return Err("codegen: integer match pattern against a non-Int subject".to_string());
f1e33d7 643
            }
f1e33d7 644
            Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 645
            Instruction::I64Const(*n).encode(body);
f1e33d7 646
            Instruction::I64Eq.encode(body);
f1e33d7 647
            Instruction::If(block_type_for(result_vt)).encode(body);
f1e33d7 648
            compile_case_body(&case.body, result_vt, body, ctx, state)?;
f1e33d7 649
            Instruction::Else.encode(body);
f1e33d7 650
            compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
f1e33d7 651
            Instruction::End.encode(body);
f1e33d7 652
            Ok(())
f1e33d7 653
        }
f1e33d7 654
        ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
f1e33d7 655
        ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
f1e33d7 656
        ast::CasePattern::Class { name, fields } => {
f1e33d7 657
            compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state)
f1e33d7 658
        }
f1e33d7 659
    }
f1e33d7 660
}
f1e33d7 661
```
f1e33d7 662
f1e33d7 663
**3j. Update `compile_variant_eq_arm`** (currently lines 923-951):
f1e33d7 664
f1e33d7 665
Replace the whole function:
f1e33d7 666
f1e33d7 667
```rust
f1e33d7 668
#[allow(clippy::too_many_arguments)]
f1e33d7 669
fn compile_variant_eq_arm(
f1e33d7 670
    name: &str,
f1e33d7 671
    subject_vt: ValType,
f1e33d7 672
    scratch_local: u32,
f1e33d7 673
    result_vt: Option<ValType>,
f1e33d7 674
    case: &ast::Case,
f1e33d7 675
    rest: &[ast::Case],
f1e33d7 676
    body: &mut Vec<u8>,
f1e33d7 677
    ctx: &LocalCtx,
f1e33d7 678
    state: &mut ModuleState,
f1e33d7 679
) -> Result<(), String> {
f1e33d7 680
    let info = ctx
f1e33d7 681
        .enum_variants
f1e33d7 682
        .get(name)
f1e33d7 683
        .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
f1e33d7 684
    if subject_vt != ValType::I32 {
f1e33d7 685
        return Err(format!("codegen: enum tag pattern '{}' against a non-enum subject", name));
f1e33d7 686
    }
f1e33d7 687
    let tag = info.tag;
f1e33d7 688
    Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 689
    Instruction::I32Const(tag).encode(body);
f1e33d7 690
    Instruction::I32Eq.encode(body);
f1e33d7 691
    Instruction::If(block_type_for(result_vt)).encode(body);
f1e33d7 692
    compile_case_body(&case.body, result_vt, body, ctx, state)?;
f1e33d7 693
    Instruction::Else.encode(body);
f1e33d7 694
    compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
f1e33d7 695
    Instruction::End.encode(body);
f1e33d7 696
    Ok(())
f1e33d7 697
}
f1e33d7 698
```
f1e33d7 699
f1e33d7 700
**3k. Update `compile_variant_constructor_arm`** (currently lines 953-1026):
f1e33d7 701
f1e33d7 702
Replace the whole function:
f1e33d7 703
f1e33d7 704
```rust
f1e33d7 705
#[allow(clippy::too_many_arguments)]
f1e33d7 706
fn compile_variant_constructor_arm(
f1e33d7 707
    name: &str,
f1e33d7 708
    fields: &[ast::CasePattern],
f1e33d7 709
    subject_vt: ValType,
f1e33d7 710
    scratch_local: u32,
f1e33d7 711
    result_vt: Option<ValType>,
f1e33d7 712
    case: &ast::Case,
f1e33d7 713
    rest: &[ast::Case],
f1e33d7 714
    body: &mut Vec<u8>,
f1e33d7 715
    ctx: &LocalCtx,
f1e33d7 716
    state: &mut ModuleState,
f1e33d7 717
) -> Result<(), String> {
f1e33d7 718
    let info = ctx
f1e33d7 719
        .enum_variants
f1e33d7 720
        .get(name)
f1e33d7 721
        .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
f1e33d7 722
    if subject_vt != ValType::I32 {
f1e33d7 723
        return Err(format!("codegen: constructor pattern '{}' against a non-enum subject", name));
f1e33d7 724
    }
f1e33d7 725
    if fields.len() != info.field_types.len() {
f1e33d7 726
        return Err(format!(
f1e33d7 727
            "codegen: constructor pattern '{}' expects {} field(s), got {}",
f1e33d7 728
            name, info.field_types.len(), fields.len()
f1e33d7 729
        ));
f1e33d7 730
    }
f1e33d7 731
    let tag = info.tag;
f1e33d7 732
    let field_types = info.field_types.clone();
f1e33d7 733
f1e33d7 734
    // A constructor pattern can only match if the runtime subject is actually
f1e33d7 735
    // a heap pointer (payload variants are always >= HEAP_BASE); a
f1e33d7 736
    // payload-free sibling variant is a small int tag, and loading i32 from
f1e33d7 737
    // that address would read unrelated/zeroed memory instead of a real tag.
f1e33d7 738
    // Guard with a range check before doing the I32Load.
f1e33d7 739
    Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 740
    Instruction::I32Const(HEAP_BASE as i32).encode(body);
f1e33d7 741
    Instruction::I32GeU.encode(body);
f1e33d7 742
    Instruction::If(BlockType::Result(ValType::I32)).encode(body);
f1e33d7 743
    Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 744
    Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
f1e33d7 745
    Instruction::I32Const(tag).encode(body);
f1e33d7 746
    Instruction::I32Eq.encode(body);
f1e33d7 747
    Instruction::Else.encode(body);
f1e33d7 748
    Instruction::I32Const(0).encode(body);
f1e33d7 749
    Instruction::End.encode(body);
f1e33d7 750
    Instruction::If(block_type_for(result_vt)).encode(body);
f1e33d7 751
    for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() {
f1e33d7 752
        let bind_name = match pat {
f1e33d7 753
            ast::CasePattern::Name(n) => Some(n.as_str()),
f1e33d7 754
            ast::CasePattern::Wildcard => None,
f1e33d7 755
            _ => return Err("codegen: only bare bindings or '_' are supported inside a constructor pattern".to_string()),
f1e33d7 756
        };
f1e33d7 757
        if let Some(n) = bind_name {
f1e33d7 758
            let idx = ctx
f1e33d7 759
                .locals
f1e33d7 760
                .get(n)
f1e33d7 761
                .copied()
f1e33d7 762
                .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
f1e33d7 763
            Instruction::LocalGet(scratch_local).encode(body);
f1e33d7 764
            let offset = ((i + 1) as u64) * 8;
f1e33d7 765
            match plum_type_to_valtype(field_ty) {
f1e33d7 766
                ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }),
f1e33d7 767
                ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }),
f1e33d7 768
                _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }),
f1e33d7 769
            }.encode(body);
f1e33d7 770
            Instruction::LocalSet(idx).encode(body);
f1e33d7 771
            ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone()));
f1e33d7 772
        }
f1e33d7 773
    }
f1e33d7 774
    compile_case_body(&case.body, result_vt, body, ctx, state)?;
f1e33d7 775
    Instruction::Else.encode(body);
f1e33d7 776
    compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
f1e33d7 777
    Instruction::End.encode(body);
f1e33d7 778
    Ok(())
f1e33d7 779
}
f1e33d7 780
```
f1e33d7 781
f1e33d7 782
- [ ] **Step 4: Run the codegen test suite**
f1e33d7 783
f1e33d7 784
Run: `cargo test -p plum-wasm-codegen --test codegen_tests`
f1e33d7 785
Expected: every test passes, including all 8 new ones from Step 1 and every pre-existing test in the file (in particular, every test that already uses explicit `return` in match/if arms must still pass unchanged — `result_vt: None` for ordinary statement position and value-position `return` handling are both untouched by this refactor).
f1e33d7 786
f1e33d7 787
- [ ] **Step 5: Run the full workspace and tree-sitter suites**
f1e33d7 788
f1e33d7 789
```bash
f1e33d7 790
cargo test --workspace
f1e33d7 791
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
f1e33d7 792
```
f1e33d7 793
f1e33d7 794
Expected: fully green.
f1e33d7 795
f1e33d7 796
- [ ] **Step 6: Commit**
f1e33d7 797
f1e33d7 798
```bash
f1e33d7 799
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
f1e33d7 800
git commit -m "fix(plum-wasm-codegen): propagate a value through tail-position match/if without explicit return"
f1e33d7 801
```
f1e33d7 802
f1e33d7 803
---
f1e33d7 804
f1e33d7 805
### Task 3: Restore `examples/match.plum` to idiomatic bare-tail style, update docs
f1e33d7 806
f1e33d7 807
**Files:**
f1e33d7 808
- Modify: `examples/match.plum`
f1e33d7 809
- Modify: `README.md`
f1e33d7 810
- Test: `plum-wasm-codegen/tests/examples_test.rs` (no changes expected, just re-run)
f1e33d7 811
f1e33d7 812
**Interfaces:**
f1e33d7 813
- Consumes: Task 2's fix (a function whose tail statement is `match`/`if` with bare-expression arms now compiles and runs correctly).
f1e33d7 814
- Produces: nothing further downstream — this is the final integration/documentation task.
f1e33d7 815
f1e33d7 816
- [ ] **Step 1: Revert `examples/match.plum`'s five functions to their natural bare-tail-expression form**
f1e33d7 817
f1e33d7 818
Replace the whole file `examples/match.plum` with:
f1e33d7 819
f1e33d7 820
```plum
f1e33d7 821
enum Color =
f1e33d7 822
  | Red
f1e33d7 823
  | Green
f1e33d7 824
  | Blue
f1e33d7 825
f1e33d7 826
enum Option =
f1e33d7 827
  | Some(Int)
f1e33d7 828
  | None
f1e33d7 829
f1e33d7 830
describeNumber(n: Int) -> Str =
f1e33d7 831
  match n
f1e33d7 832
    0 =>
f1e33d7 833
      "zero"
f1e33d7 834
    1 =>
f1e33d7 835
      "one"
f1e33d7 836
    _ =>
f1e33d7 837
      "many"
f1e33d7 838
f1e33d7 839
describeBool(b: Bool) -> Int =
f1e33d7 840
  match b
f1e33d7 841
    True =>
f1e33d7 842
      1
f1e33d7 843
    False =>
f1e33d7 844
      0
f1e33d7 845
f1e33d7 846
bindExample(n: Int) -> Int =
f1e33d7 847
  match n
f1e33d7 848
    x =>
f1e33d7 849
      x
f1e33d7 850
f1e33d7 851
describeColor(c: Color) -> Str =
f1e33d7 852
  match c
f1e33d7 853
    Red =>
f1e33d7 854
      "red"
f1e33d7 855
    Green =>
f1e33d7 856
      "green"
f1e33d7 857
    Blue =>
f1e33d7 858
      "blue"
f1e33d7 859
f1e33d7 860
describeOption(opt: Option) -> Int =
f1e33d7 861
  match opt
f1e33d7 862
    Some(v) =>
f1e33d7 863
      v
f1e33d7 864
    None =>
f1e33d7 865
      0
f1e33d7 866
f1e33d7 867
main() -> Int =
f1e33d7 868
  describeOption(Some(5))
f1e33d7 869
```
f1e33d7 870
f1e33d7 871
(This is identical to the file's content before the `return`-adding workaround, restoring the originally-intended idiomatic style now that Task 2 makes it actually compile.)
f1e33d7 872
f1e33d7 873
- [ ] **Step 2: Run the examples test suite**
f1e33d7 874
f1e33d7 875
Run: `cargo test -p plum-wasm-codegen --test examples_test`
f1e33d7 876
Expected: `match_example_compiles_and_runs_correctly` (already asserting `describeOption(Some(5)) == 5`) still passes — now genuinely exercising bare-tail-expression match arms instead of explicit `return`.
f1e33d7 877
f1e33d7 878
- [ ] **Step 3: Remove the now-fixed bullet from README's Known Gaps**
f1e33d7 879
f1e33d7 880
In `README.md`, remove this line (currently line 327):
f1e33d7 881
f1e33d7 882
```markdown
f1e33d7 883
- a function body's final statement being a `match`/`if` whose arms don't all use explicit `return` — the arm values are silently dropped instead of returned, producing invalid wasm rather than a clear error (workaround: always `return` from match/if arms in tail position)
f1e33d7 884
```
f1e33d7 885
f1e33d7 886
so the Known Gaps list reads (only the remaining, still-true items):
f1e33d7 887
f1e33d7 888
```markdown
f1e33d7 889
- string interpolation (plain, non-interpolated string literals do compile)
f1e33d7 890
- multi-subject `match` (`match a, b`)
f1e33d7 891
- 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
f1e33d7 892
- nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_`
f1e33d7 893
```
f1e33d7 894
f1e33d7 895
- [ ] **Step 4: Run the full workspace and tree-sitter suites one final time**
f1e33d7 896
f1e33d7 897
```bash
f1e33d7 898
cargo test --workspace
f1e33d7 899
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
f1e33d7 900
```
f1e33d7 901
f1e33d7 902
Expected: fully green, zero known failures.
f1e33d7 903
f1e33d7 904
- [ ] **Step 5: Commit**
f1e33d7 905
f1e33d7 906
```bash
f1e33d7 907
git add examples/match.plum README.md
f1e33d7 908
git commit -m "docs+test: restore examples/match.plum to idiomatic bare-tail style; tail-position gap fixed"
f1e33d7 909
```