Skip to content

core: match guards through typing + interp + wasm (#67)#74

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/match-guards
May 15, 2026
Merged

core: match guards through typing + interp + wasm (#67)#74
hyperpolymath merged 1 commit into
mainfrom
feat/match-guards

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Closes #67.

What changed

Match guards (if e after a pattern) now work end-to-end through ephapax-typing, ephapax-interp, and ephapax-wasm. The grammar already carries MatchArm::guard: Option<Box<Expr>> from #61 — this PR replaces the NotYetSupportedInCore stub in typing, the Unimplemented stub in interp, and adds the i32.eqz + br_if emission in wasm.

Per-crate changes

ephapax-typing

  • check_match type-checks the guard under the arm's pattern bindings; unified against Bool at the guard's span. Non-Bool guards return TypeMismatch.
  • check_exhaustiveness now skips guarded arms when building the Maranget matrix — guards can refute at runtime so they don't contribute to coverage. Some(v) if v > 0 -> ... | None -> ... is correctly reported as non-exhaustive.

ephapax-interp

  • eval_match evaluates the guard after applying the pattern bindings. false ⇒ restore bindings, continue to next arm. true ⇒ run body.
  • Failed guards must not leak bindings — verified by a test where an outer v=999 survives a refuted Some(v) arm.

ephapax-wasm

ephapax-linear

Behaviour table

Code Outcome
match x { Some(v) if v > 0 => v | Some(v) => v | None => 0 } Typechecks; covers Some entirely via the second arm.
match x { Some(v) if v > 0 => v | None => 0 } NonExhaustiveMatch { missing: "Some(_)" }.
Some(_) if 42 => ... TypeMismatch (guard is I32, expected Bool).
Some(7) if v > 0 => v Interp returns 7.
Some(-3) if v > 0 => v | _ => 0 Interp returns 0 (guard falls through).

Test plan

  • cargo test -p ephapax-typing --lib → 73 pass (was 70), +3 guard tests.
  • cargo test -p ephapax-interp --lib → 17 pass (was 14), +3 guard tests.
  • cargo test -p ephapax-wasm --lib → 79 pass (was 77), +2 guard tests (validated via wasmparser).
  • Full workspace lib tests pass.
  • CI green on all checks.

Out of scope

  • Or-patterns inside a single arm — separate feature.
  • Guards containing match — should fall out for free; no targeted test added.

🤖 Generated with Claude Code

End-to-end implementation of `MatchArm::guard` (the `if e` clause that
runs after the pattern matches and before the arm body). The
typechecker enforces `Ty::Bool`, the interpreter falls through on
`false`, codegen emits an `i32.eqz + br_if $arm_i_fail` after the
destructure.

## Typing — `ephapax-typing`

* `check_match` now type-checks the guard under the arm's pattern
  bindings; the guard's type is unified with `Bool` at the guard's
  span. Non-Bool guards produce `TypeMismatch`.
* `check_exhaustiveness` skips guarded arms when building the
  matrix — guards can refute at runtime, so they don't contribute
  to coverage. `Some(v) if v > 0 -> ... | None -> ...` is
  non-exhaustive (missing the Some-non-positive case); only
  `Some(v) -> ... | None -> ...` (no guard) covers Some entirely.

## Interp — `ephapax-interp`

* `eval_match` evaluates the guard after applying pattern bindings.
  If the guard returns `false`, the bindings are restored to their
  prior state and the next arm is tried. Failed guards must not
  leak bindings — confirmed by a regression test where an outer
  `v=999` survives a refuted inner `Some(v)` arm.
* Non-Bool guard values raise `RuntimeError::TypeError { expected:
  "Bool", found: ... }` — should be unreachable for well-typed
  programs.

## Wasm — `ephapax-wasm`

* `compile_match` emits the guard expression after the pattern
  destructure. The guard pushes an `i32` onto the stack; `i32.eqz`
  + `br_if 0` falls through to the surrounding `$arm_i_fail` block
  when the guard is false, exactly like a refuted nested pattern.
* The fall-through machinery established by #68 is reused — no new
  control-flow structure needed.

## Linear — `ephapax-linear`

* No changes. Both `linear.rs::walk_expr` and `affine.rs::walk_expr`
  already walked into `arm.guard` when computing per-arm snapshots
  (in place since the variant landed); the existing N-arm branch-
  agreement check correctly enforces consumption parity across
  guarded and non-guarded arms.

## Tests

* `cargo test -p ephapax-typing --lib` → 73 pass (was 70), +3:
  - `test_match_guard_typechecks_with_pattern_binding` — guard
    reads `v` bound by `Some(v)`.
  - `test_match_guard_non_bool_rejected` — guard of type `I32`
    rejected as `TypeMismatch`.
  - `test_match_guarded_arm_not_counted_for_exhaustiveness` —
    `Some(v) if v > 0 | None` produces `NonExhaustiveMatch`.
* `cargo test -p ephapax-interp --lib` → 17 pass (was 14), +3:
  - `test_eval_match_guard_pass` — `Some(7) if v > 0 => v` ⇒ 7.
  - `test_eval_match_guard_fail_falls_through` — `Some(-3) if v > 0`
    falls through to wildcard ⇒ 0.
  - `test_eval_match_failed_guard_does_not_leak_bindings` —
    refuted arm's binding is reverted before the next arm runs.
* `cargo test -p ephapax-wasm --lib` → 79 pass (was 77), +2:
  - `compile_module_match_guard_simple` — `Some(v) if v > 0 | Some(_)
    | None`, validates via `wasmparser`.
  - `compile_module_match_guard_on_wildcard` — guard on a wildcard
    catch-all, validates via `wasmparser`.

## Out of scope

* Or-patterns inside a single arm — separate feature.
* Guard expressions that themselves contain `match` — should fall
  out for free; no targeted test added.

Closes #67.
@hyperpolymath hyperpolymath merged commit 3a16888 into main May 15, 2026
11 checks passed
@hyperpolymath hyperpolymath deleted the feat/match-guards branch May 15, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: match guards (if e after pattern) — wire typing + interp + wasm

1 participant