plum

#treesitter#compiler#wasm

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

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


docs/superpowers/specs/2026-07-20-tail-position-and-grammar-gaps-design.md
1ccf9bd 1
# Fix two pre-existing gaps: grammar trailing-statement limitation, codegen tail-position value drop
1ccf9bd 2
1ccf9bd 3
## Problem
1ccf9bd 4
1ccf9bd 5
Two real, pre-existing defects were discovered (and worked around, not fixed) while implementing
1ccf9bd 6
general enum support:
1ccf9bd 7
1ccf9bd 8
1. **Grammar**: a multi-line indented function/method body's `_statement` rule
1ccf9bd 9
   (`tooling/tree-sitter-plum/grammar.js:166-179`) only accepts `$.primary_expression` as one of
1ccf9bd 10
   its alternatives, not the full `$.expression`. A bare comparison (`a == b`), boolean operator
1ccf9bd 11
   (`a && b`), or ternary (`a ? b : c`) used as a statement — most commonly the final line of an
1ccf9bd 12
   indented body — fails to parse: the operator is dropped and the parser emits an `ERROR` node,
1ccf9bd 13
   silently splitting what should be one expression into two separate statements.
1ccf9bd 14
1ccf9bd 15
2. **Codegen**: `plum-wasm-codegen`'s `compile_block_as_fn_body` (`plum-wasm-codegen/src/lib.rs`)
1ccf9bd 16
   only preserves a function's return value when the body's *literal* last statement is a bare
1ccf9bd 17
   `ast::Stmt::Expr`. A `Stmt::If` or `Stmt::Match` in that same tail position compiles each arm as
1ccf9bd 18
   an ordinary statement block (`BlockType::Empty`), so a bare tail expression inside an arm is
1ccf9bd 19
   `Drop`ped instead of left as the function's result. This produces wasm that fails
1ccf9bd 20
   `wasmparser::validate` — a loud failure in the test suite, but `plum-cli`'s `compile` subcommand
1ccf9bd 21
   (`plum-cli/src/main.rs`) writes `compile_source`'s output bytes to disk without validating them,
1ccf9bd 22
   so a real user hitting this today gets a corrupt, unusable `.wasm` file with no error message at
1ccf9bd 23
   all, violating the README's stated guarantee that codegen "reports a clear error rather than
1ccf9bd 24
   silently producing wrong code."
1ccf9bd 25
1ccf9bd 26
Both were confirmed by direct reproduction during a prior session (parse-tree dump for gap 1;
1ccf9bd 27
`wasmparser::validate` failure plus a fixed reproduction using explicit `return` for gap 2).
1ccf9bd 28
1ccf9bd 29
## Fix 1: grammar
1ccf9bd 30
1ccf9bd 31
`$.expression` (`grammar.js:261-269`) is already a strict superset of `$.primary_expression`
1ccf9bd 32
(`grammar.js:271-285`) — every existing alternative reachable through `primary_expression` remains
1ccf9bd 33
reachable through `expression`, and `expression` is already used without incident elsewhere in the
1ccf9bd 34
grammar (`assign`'s values, `assert`, `return`, `if`'s condition). The fix is a single-line change:
1ccf9bd 35
1ccf9bd 36
```js
1ccf9bd 37
_statement: ($) =>
1ccf9bd 38
  choice(
1ccf9bd 39
    $.assign,
1ccf9bd 40
    $.break,
1ccf9bd 41
    $.continue,
1ccf9bd 42
    $.assert,
1ccf9bd 43
    $.for,
1ccf9bd 44
    $.while,
1ccf9bd 45
    $.if,
1ccf9bd 46
    $.match,
1ccf9bd 47
    $.return,
1ccf9bd 48
    $.todo,
1ccf9bd 49
    $.expression,   // was: $.primary_expression
1ccf9bd 50
  ),
1ccf9bd 51
```
1ccf9bd 52
1ccf9bd 53
Regenerate the parser and run the full existing corpus suite to confirm zero regressions (every
1ccf9bd 54
prior `primary_expression`-shaped statement remains valid, since `expression` accepts it too), then
1ccf9bd 55
add new corpus cases proving a bare comparison, boolean-operator, and ternary expression now parse
1ccf9bd 56
as a single statement with no `ERROR` node when used as a body's trailing line.
1ccf9bd 57
1ccf9bd 58
## Fix 2: codegen tail-position value propagation
1ccf9bd 59
1ccf9bd 60
Add a "value position" compile path that mirrors the existing `Stmt::Expr` special-case in
1ccf9bd 61
`compile_block_as_fn_body`, but recursively for `Stmt::If` and `Stmt::Match`:
1ccf9bd 62
1ccf9bd 63
- A `Stmt::If`/`Stmt::Match` in **value position** (the function body's literal last statement, or
1ccf9bd 64
  recursively the last statement of an `if`/`else if`/`else` branch or `match` arm that is itself in
1ccf9bd 65
  value position) compiles its condition/subject as today, but compiles each branch/arm using
1ccf9bd 66
  `BlockType::Result(result_vt)` instead of `BlockType::Empty`, and compiles that branch's own last
1ccf9bd 67
  statement through the same value-position path (recursing into further nested `If`/`Match`, or
1ccf9bd 68
  terminating at a bare `Stmt::Expr` — left on the stack, not dropped — or `Stmt::Return`/`Stmt::Todo`
1ccf9bd 69
  — already stack-polymorphic in wasm, since control never falls through past them).
1ccf9bd 70
- Every branch of a value-position `If` must have an `else` (a value can't be produced on a
1ccf9bd 71
  path that doesn't exist); every `match` arm must resolve to one of the shapes above. If any
1ccf9bd 72
  branch/arm's tail statement is some other shape (loop, assignment, etc.) that cannot yield a
1ccf9bd 73
  value, `compile_source` returns a clear `Err` (e.g. `"codegen: function 'f' has a control-flow
1ccf9bd 74
  path that doesn't produce a return value"`) — never silently falls through to today's
1ccf9bd 75
  invalid-wasm behavior.
1ccf9bd 76
- Existing callers of `compile_block_as_fn_body` are unaffected: the recursive value-position
1ccf9bd 77
  logic only activates when `has_return_value` is true and the function's tail statement actually
1ccf9bd 78
  is `If`/`Match` (today it already special-cases plain `Stmt::Expr`; this generalizes the same
1ccf9bd 79
  idea one level of control flow deeper). `If`/`Match` appearing anywhere *except* value position
1ccf9bd 80
  (e.g. as a non-tail statement, or a tail statement in a `Unit`-returning function) keep their
1ccf9bd 81
  existing, unchanged compilation path.
1ccf9bd 82
1ccf9bd 83
## Testing plan
1ccf9bd 84
1ccf9bd 85
- **Grammar**: extend `tooling/tree-sitter-plum/test/corpus/` with cases for a bare comparison,
1ccf9bd 86
  boolean-operator, and ternary expression as a body's trailing statement; run the full corpus
1ccf9bd 87
  suite to confirm no regressions.
1ccf9bd 88
- **Checker**: no changes are needed to `plum-checker` for either fix (both are purely
1ccf9bd 89
  parser/codegen concerns) — but re-run the full checker suite (including `examples_test.rs`) to
1ccf9bd 90
  confirm nothing regresses, since the grammar change affects what `plum-core`'s parser produces.
1ccf9bd 91
- **Codegen**: add tests (`plum-wasm-codegen/tests/codegen_tests.rs`) executing (via `wasmtime`,
1ccf9bd 92
  not just validating) functions whose tail statement is: a `match` with bare-expression arms and
1ccf9bd 93
  no explicit `return` (the original `examples/match.plum` shape that motivated this work); an `if`
1ccf9bd 94
  with bare-expression branches and no explicit `return`; a *nested* `if` inside a `match` arm
1ccf9bd 95
  (recursion through both control-flow kinds); a mix of one arm using explicit `return` and
1ccf9bd 96
  another using a bare tail expression (validates the stack-polymorphism claim); and a
1ccf9bd 97
  deliberately malformed case (a `match` arm whose tail statement is e.g. a bare `Stmt::Assign`)
1ccf9bd 98
  asserting `compile_source` returns the new clear `Err` rather than ever producing bytes.
1ccf9bd 99
- **Examples**: revert the `return`-adding workaround applied to `examples/match.plum`'s five
1ccf9bd 100
  functions back to their natural bare-tail-expression form (proving the fix actually restores the
1ccf9bd 101
  originally-intended, more idiomatic style) and confirm `plum-wasm-codegen/tests/examples_test.rs`
1ccf9bd 102
  still passes.
1ccf9bd 103
- **README**: remove the now-fixed "final statement being a match/if without explicit return"
1ccf9bd 104
  bullet from Known Gaps.
1ccf9bd 105
1ccf9bd 106
## Out of scope
1ccf9bd 107
1ccf9bd 108
- General "does every code path return a value" checking beyond what's needed to make
1ccf9bd 109
  value-position `If`/`Match` either compile correctly or fail with a clear error — this is not a
1ccf9bd 110
  full control-flow/definite-assignment analysis in `plum-checker`, just a defensive check inside
1ccf9bd 111
  the new codegen path.
1ccf9bd 112
- `plum-cli`'s `compile` command validating its own output before writing to disk — worth doing
1ccf9bd 113
  separately, but this fix makes the underlying codegen bug (the actual source of the corrupt
1ccf9bd 114
  output) go away rather than adding a downstream safety net. Not required for this fix to be
1ccf9bd 115
  complete, since the root cause is resolved.
1ccf9bd 116
- Generics monomorphization — a separate, much larger effort, tracked independently.