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-24-guard-clause-match-design.md
# Guard-Clause Match Arms — Design Spec

## Goal

Add a second form of `match` expression: one with no subject expression, where
each arm is a boolean guard condition instead of a value pattern, e.g.:

```
compare(x: Int, y: Int) -> Ordering =
  match
    | x > y => GT
    | x == y => EQ
    | _ => LT
```

This lets a function express a cascade of independent boolean conditions
without threading a placeholder subject through `match` or writing an
`if`/`else if`/`else` chain by hand. It is purely additive: the existing
subject-ful `match` form (`match expr \n  pattern => body`) is completely
unchanged.

## Non-goals

- No new stdlib types (no `Ordering` enum) — `GT`/`EQ`/`LT` in the example
  above are illustrative; a caller brings their own type.
- No exhaustiveness *proof* for guard matches — guard conditions are
  arbitrary runtime booleans, not statically enumerable like an enum's
  variants, so there is no compile-time exhaustiveness check for this form
  (mirrors how today's pattern-match exhaustiveness check is itself only a
  codegen-time affordance for enums with no wildcard, not a hard error).
- No change to the `fn` keyword, parameter syntax, or case-body arrow
  (`=>`) — this spec is scoped to guard-clause arms only, reusing every
  other piece of existing syntax as-is.

## Current state

- `match`'s grammar (`tooling/tree-sitter-plum/grammar.js`, `match` rule) requires
  one or more subject expressions (`commaSep1(field("subject", $.expression))`,
  no `optional()`); there is no subject-less form today.
- `case`'s patterns (`case_pattern` rule) are literals, constructor patterns,
  dotted names, bare names, or `_` — never a full boolean expression like `x > y`.
- `plum-core/src/ast.rs`'s `CasePattern` enum has no variant for an arbitrary
  expression.
- `plum-checker`'s `check_match` type-checks each case's patterns against the
  subject types 1:1, erroring if the counts don't match; there is no code path
  for zero subjects today.
- `plum-wasm-codegen`'s `compile_match` always evaluates and scratch-stores
  every subject, then dispatches per-pattern-kind (int/string/float/class/name/
  wildcard) with constructor-fallthrough logic — considerably more machinery
  than a guard match needs.

## Design

### Grammar

`match` gains a second alternative via `choice`, disambiguated by whether the
line has a subject expression before the indent:

```js
match: ($) =>
  prec.left(
    choice(
      seq(
        "match",
        commaSep1(field("subject", $.expression)),
        $._indent,
        repeat(field("case", $.case)),
        $._dedent,
      ),
      seq(
        "match",
        $._indent,
        repeat(field("case", $.guard_case)),
        $._dedent,
      ),
    ),
  ),

guard_case: ($) =>
  seq(
    "|",
    field("guard", choice($.expression, "_")),
    "=>",
    field("body", choice($.expression, $.body)),
  ),
```

The existing `case`/`case_pattern`/`class_pattern` rules are untouched. Guard
arms are a structurally distinct rule (`guard_case`), not a new
`case_pattern` alternative — a guard is a full expression (`x > y`, `x == y`),
never a literal/constructor/name pattern, so keeping the two arm kinds as
separate grammar rules avoids any ambiguity between "is this a pattern or an
expression" at parse time.

### AST

One new variant on the existing `CasePattern` enum
(`plum-core/src/ast.rs`):

```rust
pub enum CasePattern {
    Class { name: String, fields: Vec<CasePattern> },
    String(String),
    Int(i64),
    Float(f64),
    Name(String),
    Wildcard,
    Guard(Expr),
}
```

No new top-level struct. A subject-less `match` parses to the existing
`Match { subjects: vec![], cases }` shape, where each `Case` has exactly one
pattern: either `CasePattern::Guard(expr)` for a boolean condition, or
`CasePattern::Wildcard` for a bare `_` guard arm (the parser recognizes the
literal `_` token in guard position and emits the existing `Wildcard`
variant — not a `Guard` wrapping a trivial "true" expression — so every other
part of the pipeline that already knows how to handle `Wildcard` needs no new
knowledge of "guard-flavored wildcards").

`plum-core/src/parser.rs` gains a `guard_case`-parsing branch (parallel to
today's `case` parsing) that builds a `Case` with one `CasePattern::Guard` or
`CasePattern::Wildcard` pattern from each `guard_case` node, and `parse_match`
is extended so an empty subject list is valid (today's subject-collection
logic already just slices "named children before the first case node," so an
empty slice naturally falls out — no structural change needed there, only the
grammar's `optional`-equivalent `choice` unlocks it).

### Checker

`check_match` (`plum-checker/src/lib.rs`) branches on `m.subjects.is_empty()`:

- **Subject-ful (today's path, unchanged):** infer each subject's type, zip
  patterns against subject types 1:1, `check_pattern` as today.
- **Guard (new path):** for each case (exactly one pattern per case, enforced
  by the grammar), if the pattern is `CasePattern::Guard(expr)`, infer `expr`'s
  type via `infer_expr` and unify it against `PlumType::TBool`, producing a
  type error if a guard isn't boolean (e.g. `| x + 1 => ...`). If the pattern
  is `CasePattern::Wildcard`, no type-check is needed (matches everything, same
  as today's wildcard-pattern meaning). Every arm's body is still
  `check_block`-ed against `declared_ret` exactly as today — this already
  enforces that all arms return a consistent type, no new "unify across arms"
  step is needed.

### Codegen

`plum-wasm-codegen` compiles a guard match as an if/else-if chain, reusing
whatever codegen the `if`/`else if`/`else` statement already uses (not routed
through `compile_match_arms_multi`'s subject-scratch-slot machinery, which
this form has no use for — no subject to evaluate/store, no destructuring, no
per-pattern-kind branching). Each `CasePattern::Guard(expr)` arm compiles its
guard as the `if`/`else if` condition and its body as that branch's body; a
`CasePattern::Wildcard` arm (if present) becomes the chain's trailing `else`.
If no `Wildcard` arm is present and every guard evaluates false at runtime,
the chain falls through to `Unreachable` (a trap) — consistent with today's
non-exhaustive pattern-match behavior, just determined at runtime instead of
compile time, since guard conditions aren't statically enumerable.

### Testing strategy

- Tree-sitter corpus (`tooling/tree-sitter-plum/test/corpus/match.txt`): add
  cases for a subject-less guard match (with and without a trailing `_`
  arm), verifying the new `guard_case` node shape.
- `plum-core` parser tests: add coverage for the new `guard_case` parsing
  path (note: the survey found no dedicated existing `match` test in
  `parser_test.rs` today — this is a good opportunity to add the first one,
  scoped to this feature).
- `plum-checker` tests: a guard match with all-boolean guards type-checks;
  a non-boolean guard (e.g. `| x + 1 => ...`) produces a type error; a guard
  match with inconsistent arm-body return types produces the same kind of
  error today's subject-ful match already produces for that case.
- `plum-wasm-codegen` tests: a guard match with a `_` fallback runs and
  returns the expected arm's value for several inputs; a guard match with NO
  `_` fallback traps at runtime when no guard is true (mirrors the existing
  non-exhaustive-match trap test pattern already used for enum matches,
  per the survey's note on `codegen_tests.rs`'s existing non-exhaustive-match
  coverage).