feat: MAT001 — non-exhaustive Result match (?bs 0.9)#79
Open
marcelofarias wants to merge 7 commits into
Open
Conversation
Fires when a match explicitly handles ok or err but omits the opposing tag without a wildcard arm. Closes the second half of the result-contract loop: UNS005 enforces that stdlib calls are wrapped in match; MAT001 enforces that the match is exhaustive. Closes #78. Co-Authored-By: Botkowski <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new compiler diagnostic MAT001 (gated behind ?bs 0.9) to detect non-exhaustive match expressions that handle only one of ok/err without a wildcard arm, and wires the new rule through compiler + MCP “explain” surfaces.
Changes:
- Introduces
passMatCheckto scanmatchexpressions and throw MAT001 whenok/errcoverage is incomplete (no_arm). - Registers MAT001 in the compiler error-code registry and MCP explanation registry, plus updates MCP known-codes test coverage.
- Adds a dedicated compiler test suite for MAT001 behavior across positive/negative cases and version gating.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/compiler/src/passes/mat-check.ts | New MAT001 compiler pass that detects non-exhaustive Result-style matches. |
| packages/compiler/src/transform.ts | Adds matCheck into the compiler pass pipeline for ?bs 0.9+. |
| packages/compiler/src/error-codes.ts | Registers MAT001 metadata (rule/idiom/rewrite/example) for diagnostics and bs explain. |
| packages/compiler/tests/mat-check.test.ts | Adds tests validating MAT001 triggering, suppression, and version gating. |
| packages/mcp/src/explanations.ts | Adds MCP “explain” entry for MAT001 (long-form documentation + examples). |
| packages/mcp/tests/server.test.ts | Updates known diagnostic code list to include MAT001. |
| CHANGELOG.md | Documents MAT001 addition under ?bs 0.9 “Added”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+56
to
+67
| throw new BotscriptError([{ | ||
| code: "MAT001", | ||
| severity: "error", | ||
| file: null, | ||
| line, | ||
| column, | ||
| start: matchStart, | ||
| end: tokens[expr.end - 1]?.end ?? matchStart, | ||
| message: `match on Result is missing '${missing}' arm — add '${missing} { ... } -> ...' or a wildcard '_ -> ...' arm`, | ||
| rule: entry.rule, | ||
| idiom: entry.idiom, | ||
| rewrite: entry.rewrite, |
| "prefer explicit `ok` and `err` arms over a wildcard when the error type carries useful context — " + | ||
| "a wildcard silently discards the payload", | ||
| rewrite: | ||
| "add 'err { e } -> ...' arm or '_ -> ...' wildcard", |
Comment on lines
+458
to
+465
| "**Suppression mechanisms (in order of preference):**\n\n" + | ||
| "1. **Explicit err arm** — handle the error case directly:\n" + | ||
| " ```\n match http.get(url) {\n ok { value } -> ok(value.body)\n err { e } -> err(e.message)\n }\n ```\n\n" + | ||
| "2. **Wildcard arm** — use `_` when you want to coerce or ignore the missing case:\n" + | ||
| " ```\n match http.get(url) {\n ok { value } -> ok(value.body)\n _ -> err(\"request failed\")\n }\n ```\n\n" + | ||
| "The check is scoped to the `ok`/`err` tag vocabulary — it fires only when at least one " + | ||
| "of those tags is explicitly named in an arm. User-defined tagged unions with different " + | ||
| "tag names are not affected.", |
The rewrite field was hardcoded to suggest adding an err arm, which was misleading when the ok arm was the missing one. Now constructs the hint dynamically from the missing tag, and updates the registry entry and MCP explanation to cover both missing-ok and missing-err cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| */ | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
| import { transform, BotscriptError } from "../src/transform.js"; |
| line, | ||
| column, | ||
| start: matchStart, | ||
| end: tokens[expr.end - 1]?.end ?? matchStart, |
… BotscriptError import Copilot review (round 2) flagged two issues: - end span covered the full match block; now anchors at the match keyword token end, consistent with how thr-check and bare-as anchor their diagnostics - BotscriptError was imported from transform.js (which doesn't export it), causing a potential ESM load-time crash; removed since it was unused Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+78
to
+86
| it("does not fire when a wildcard arm is present (no ok)", () => { | ||
| const src = | ||
| "?bs 0.9\n" + | ||
| "fn fetchData(url: string) uses { net } -> Result<string, string> {\n" + | ||
| " match http.get(url) {\n" + | ||
| " ok { value } -> ok(value)\n" + | ||
| " _ -> err(\"failed\")\n" + | ||
| " }\n" + | ||
| "}\n"; |
Comment on lines
+90
to
+98
| it("does not fire when a wildcard arm is present (no err)", () => { | ||
| const src = | ||
| "?bs 0.9\n" + | ||
| "fn fetchData(url: string) uses { net } -> string {\n" + | ||
| " match http.get(url) {\n" + | ||
| " err { e } -> e\n" + | ||
| " _ -> \"default\"\n" + | ||
| " }\n" + | ||
| "}\n"; |
…n cases Test names said "(no ok)" and "(no err)" but had the logic backwards — each test includes one of the ok/err arms; the wildcard covers the *other*. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+445
to
+449
| MAT001: { | ||
| code: "MAT001", | ||
| title: "non-exhaustive match on Result — missing ok or err arm", | ||
| body: | ||
| "From `?bs 0.9`, a `match` expression that explicitly handles the `ok` or `err` tag " + |
Copilot review correctly flagged that MAT001 was missing from both the AGENTS.md diagnostic codes table and the README MCP `explain` tool code list. Added the row and updated the explain description. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Make suppression examples in MCP MAT001 explanation symmetric — step 1 now shows adding the missing arm for both the err-absent and ok-absent cases - 596/596 tests pass (previously failing due to stale dist — rebuild resolves it)
Comment on lines
+102
to
+110
| it("does not fire for a match with no ok/err arms (user-defined tags)", () => { | ||
| const src = | ||
| "?bs 0.9\n" + | ||
| "fn classify(x: number) -> string {\n" + | ||
| " match x {\n" + | ||
| " 1 -> \"one\"\n" + | ||
| " _ -> \"other\"\n" + | ||
| " }\n" + | ||
| "}\n"; |
Comment on lines
+10
to
+16
| - **MAT001 — non-exhaustive Result match.** | ||
| From `?bs 0.9`, a `match` expression that explicitly handles the `ok` or `err` | ||
| tag must also handle the opposing tag (or include a wildcard `_` arm). Fires | ||
| when the `ok`/`err` tag vocabulary is used but one side is left unhandled. | ||
| Suppression: add the missing arm explicitly, or use a wildcard `_` arm. | ||
| The check is scoped to the `ok`/`err` vocabulary — user-defined tagged unions | ||
| with other tag names are unaffected. |
| column, | ||
| start: matchStart, | ||
| end: tokens[expr.start]!.end, | ||
| message: `match on Result is missing '${missing}' arm — add '${missing} { ... } -> ...' or a wildcard '_ -> ...' arm`, |
Comment on lines
+75
to
+77
| // matCheck: exhaustiveness check on Result match (MAT001) — fires when a | ||
| // match explicitly handles ok or err but omits the other without a wildcard. | ||
| { name: "matCheck", fn: passMatCheck, minVersion: "0.9" }, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
?bs 0.9, fires when amatchexpression explicitly handlesokorerrbut omits the opposing tag without a wildcard_arm.ok/errtag vocabulary is used.ok/errtag vocabulary — user-defined tagged unions are unaffected.Closes #78.
How it works
After
passMatchhas parsed the source,passMatCheckscans allmatchexpressions:wildcardpattern → skip (exhaustive by wildcard).okorerrtag patterns but not both → MAT001 with a rewrite hint.matchkeyword token, consistent with other pass diagnostics.Test plan
pnpm -r build && pnpm test— passesmatchwithokarm, noerrarm → MAT001matchwitherrarm, nookarm → MAT001match await ...with onlyok→ MAT001okanderrarms → cleanokarm + wildcard_arm → cleanerrarm + wildcard_arm → cleanok/errtag arms → clean?bs 0.9→ no MAT001bs explain MAT001returns rule/idiom/rewrite🤖 Generated with Claude Code