VIP-7: match is TC39 pattern matching, plus a compile-time exhaustiveness check
Status: accepted by Will, 2026-10-28.
Decision
- Clarified 2026-11-03: throw arms follow VIP-4. A
throwarm fails the Effect in effect code, rejects the Promise in anasync match, and isEK0401in other synchronous VibeLang code. The async-match boundary covers its subject, tests and arms; nested source callables keep their own contexts.
match (e) {
when NotFound: guest; // NotFound's Symbol.customMatcher decides, as TC39 says
when Option.Some(let v): v;
when DbError and if (retryable): retry();
default: fallback;
}match,when,is, patterns, guards,and/or/notanddefaultare exactly the TC39 pattern-matching proposal, pinned to a dated snapshot. We move to a newer snapshot deliberately, never automatically.- The full proposal is implemented, not today's subset (D207): regular-expression and relational patterns and
async matchincluded. - One addition: a
matchwith nodefaultover a finite union that misses a case is a compile error. The check only reports errors; it never changes what runs. - Class arms use the class's
Symbol.customMatcher. VibeLang'serrorclasses get a matcher that tests_tag, sowhen NotFound:matches across copies of a package. - Clarified 2026-11-03: the matcher key. The runtime initialization authorized by VIP-31 defines the global
Symbol.customMatcherwhen absent asSymbol.for("Symbol.customMatcher"), with the descriptors and initialization order in Runtime guarantees. An existing host symbol keeps its identity; own matchers and runtime helpers use that actual key. Native counterparts choose the host key when present and the registry key otherwise. This does not patch Effect or install matcher methods on built-in constructors. - Condition: no run-time patching of Effect. Until Effect ships custom matchers for
Option,Exit,Resultand its tagged errors, the compiler keeps today's fixed checks for them (D208). We propose the matchers upstream. - Amended 2026-10-29: what the exhaustiveness check covers. Literal unions,
boolean,dataunions,errorunions and enums. A guarded arm covers nothing. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00. - Amended 2026-10-29: the retained tagged-error checks. The retained checks for Effect's tagged errors are each module's own guard, as D208 compiles
Option.SometoOption.isSome: twelve effect 4.1.1 constructors with a literal_tagand a companionisXguard (Cause.TimeoutError→Cause.isTimeoutError(s),Schema.SchemaError→Schema.isSchemaError(s), and the others listed in the spec's Pattern matching chapter). Each is recognized by resolved binding, aliases included and shadowing excluded. A bare pattern runs the guard; an extractor matches[s]; arity mismatch is a failed match. The subject's static type never chooses the test. Every other constructor uses ordinary class matching (amended below). A VIP-32 Effect version bump regenerates the list. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 53). - Amended 2026-10-29:
async matchin effect code.async matchin effect code is a plain async boundary inside its ownasyncarrow, run through an explicit bridge:try promise(() => async match (b) { … }). Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 65). - Amended 2026-10-30: class patterns on ordinary classes test
instanceof. The pinned draft's inheritedFunction.prototypematcher reads a[[ConstructedBy]]internal slot that only an engine can record, so compiled JavaScript can't emulate it for classes and objects from untouched.tsor.jscode. A class with its ownSymbol.customMatcher(VibeLangerror,schema errorand generated matchers, and the retained Effect checks above) uses that matcher; any other class matches whensubject instanceof C, then destructures as the draft says. No helper patchesFunction.prototypeor records construction. So an object whose prototype changed after construction doesn't match its class, andObject.create(C.prototype)does; when engines ship the draft's matcher, VibeLang follows it in a VIP. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67). - Amended 2026-10-30: built-in class patterns. A class pattern naming a built-in uses
Array.isArray(subject)forArrayandsubject instanceof Cfor every other built-in (Errorand its subclasses,AggregateError,Date,RegExp,Map,Set,WeakMap,WeakSet,Promise, typed arrays,ArrayBuffer), because compiled JavaScript can't read the draft's internal slots portably. A cross-realm built-in other than an array failsinstanceof. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67a). - Amended 2026-10-30: a class pattern on a VibeLang value that is not a class. A class pattern naming a
datadeclaration or variant constructor, aschemadeclaration, aserviceor aContext.Referenceis a compile error (EK0702; a service keepsEK3004). Onlyerrordeclarations and the retained Effect checks have their ownSymbol.customMatcher. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67b). - Amended 2026-10-29: which subjects are checked, and which arms cover a case. The subject's type is normalized into union members (
booleantotrue | false, an enum to its members, adatatype to its variants,OptiontoNone | Some,ExitandResulttoSuccess | Failure). The check applies when every member is a literal,null,undefined, an enum member, adatavariant, anerrororschema errorinstance, one of those six cases or a retained Effect error; any other member turns it off. Only unguarded arms cover; a bare binding covers everything; an object pattern covers variantVwhen it has_tag: "V"and its other properties are bare keys orletbindings of required fields;orcovers the union;and,not, regex, relational, array, built-in and user-matcher patterns cover nothing. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 78). - Amended 2026-10-30: lexical cell identity. Alternatives of one
orshare the cell of a name bound with the same keyword. A later alternative may replace a cell initialized by a failed earlier alternative; saved closures observe the replacement. Sourceconstassignments remain forbidden; ordinary TDZ and duplicate-initialization rules outside that authorized replacement remain. - Amended 2026-10-30: loop-head binding lifetimes. Lexical
isbindings inwhile/do…whileshadow outer names in a loop-head scope and never leak outside the loop. Every complete condition evaluation, including aftercontinue, starts with fresh uninitialized cells. A successful condition and its following body share those cells, including laterletwrites seen by closures; short-circuiting can leave a binding in TDZ even when the condition is true. The initialdobody uses uninitialized head cells; closures captured there retain those cells permanently in TDZ. Later conditions never rebind the initial cells. Nested source scopes and ordinaryifblock scope retain their existing rules. - Amended 2026-10-30: the other binding homes, and use before assignment. In
for (init; test; update), aninitbinding belongs to the loop head as aletorconstdeclared ininitwould, copied into each iteration when it islet; atestorupdatebinding is a fresh cell in each iteration environment. Afor…in,for…of,for await,for concurrentorfor tryhead expression's binding is copied into each iteration. A concise arrow body's binding belongs to the arrow body, and a field initializer's binding to that initializer; anenummember initializer has no home, so a binding there isEK0705.isandmatchbindings take part in TypeScript's definite-assignment and use-before-declaration checks (TS2454,TS2448); a read those checks can't see throwsReferenceErrorwhile the cell is uninitialized. - Amended 2026-11-03: unbraced statement bodies (replaces the 2026-10-31 amendment). The statement body of
if,else,for,for…in,for…of,for await,for concurrent,for try,while,do…whileor a labeled statement, when it is not a Block, cannot be the lexical home of anispattern binding: reportEK0705, matching TypeScript'sTS1156for aletdeclaration in that position. The quick fix andvibelang --fixwrap that statement body in braces. Anifhead belongs to the home that contains theifstatement; anelse ifhead therefore needs braces around theelsebody when it binds. A binding inside an explicitdoblock, match arm, callable body or loop head keeps its own home. Will decided this rule on 2026-11-03; it replaces detail 245's implicit-body home. - Amended 2026-10-31: one name per home. The pinned duplicate-initialization rules and their
orexception apply within one top-level pattern, as the explainer's scope sentence says. Two top-level patterns that bind one name in the same lexical home are a redeclaration, as twoletdeclarations are:TS2451, orTS2300beside a parameter. Separate blocks, match arms and the other binding homes are separate homes. The diagnostic offers to rename the later binding and its uses, andvibelang --fixapplies that rename. This rejects programs the previous statement-local compiler accepted, so the codemod ships with the rule. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 245).
// compiler-gap: pending VIP-7
function loopReaders() {
const x = 99; let n = 0; const readers: (() => number)[] = []
while (n < 2 && (++n is let x)) { readers.push(() => x); x += 10 }
return [x, ...readers.map(f => f())] // [99, 11, 12]
}
function alternativeReader() {
let saved: () => number = () => 0
const matched = [5] is ([let x and if ((saved = () => x, false))] or { length: let x })
return [matched, saved()] // [true, 1]
}Native lexical-cell witnesses for these fixed inputs preserve each closure's cell identity:
import { Effect } from "effect"
function loopCells() {
const x = 99
const readers: (() => number)[] = []
for (const input of [1, 2]) { let x = input; readers.push(() => x); x += 10 }
return [x, ...readers.map(f => f())]
}
const loopCellsExit = Effect.runSyncExit(Effect.sync(loopCells)) // Success([99, 11, 12])
let initialReader: () => number = () => 0
initialHead: {
initialReader = (): number => x
break initialHead
const x = 0
}
const initialHeadExit = Effect.runSyncExit(Effect.sync(initialReader)) // Die(ReferenceError); later cells cannot initialize this closed-over binding
function alternativeCellWitness() {
let shared = 5
const saved = () => shared
shared = 1
return [true, saved()]
}
const alternativeCellExit = Effect.runSyncExit(Effect.sync(alternativeCellWitness)) // Success([true, 1])-
Amended 2026-10-29: static typing. Patterns type as the TypeScript counterpart the specification shows for them: a
matchhas the union of its arm types (athrowarm contributesnever), the subject narrows in each arm as the arm's test narrows in TypeScript, and eachletbinding has the type TypeScript gives the bound value in the counterpart. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 79). -
Amended 2026-10-29: a lint for extractors that can never match. When a class or fixed pattern's extracted list has a statically known length and an extractor without a rest pattern has a different number of arguments, VibeLang reports a VIP-38 lint warning (
EK0703), with the quick fixeswhen X:andwhen X(let e):. Matching is unchanged. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 82). -
Amended 2026-10-29: the complete inventory of retained fixed checks. The retained fixed checks are eighteen:
Option.Some,Option.None,Exit.Success,Exit.Failure,Result.Success,Result.Failure, and the twelve tagged errors of the amendment above, each recognized by resolved binding and tested with its module's own guard. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 86). -
Amended 2026-10-29: primitive class patterns.
String,Number,Boolean,BigIntandSymbolpatterns test the primitive or its boxed object,typeof s === "string" || s instanceof String, and extract[s]for a primitive and[s.valueOf()]for a boxed object. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 87). -
Amended 2026-10-29: subpath tagged errors. The retained tagged-error checks cover every public
effect/…module, not only the root: 21 more constructors (AiError,McpClientError,CryptoError,CsvError,EncodingError,JsonArrayError,NdjsonError,TarError,XmlError,ZipError,GraphQLClientError,CookiesError,HeaderParseError,HttpBodyError,HttpClientError,HttpServerError,MultipartError,DatagramSocketError,SocketError,SqlError,WorkerError), each tested with its module'sisXguard and extracting[s]. The inventory is 39 fixed checks, 33 of them tagged errors. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 92). -
Amended 2026-11-03: the pinned
JoseError. Effect 4.1.1 addseffect/jose, whoseJoseErroris a tagged error with the guardisJoseError. The 2026-10-29 rule covers every publiceffect/…module, so this check joins the inventory, tested with its module's guard and extracting[s]: 40 fixed checks, 34 of them tagged errors. This applies that rule to the VIP-32 pin; it is no new personal answer from Will. -
Amended 2026-10-29: inherited static matchers stay unused. A class pattern uses
Symbol.customMatcheronly when it is the class's own property. Reading an inherited one would make a.tsclass Sub extends NotFound {}match everyNotFound, because the generated matcher tests the base error's_tag. Changing this needs an amendment with new evidence. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 93). -
Amended 2026-10-29: a class pattern naming a non-class callable dispatches on its static signatures. For a callee with no own
Symbol.customMatcherthat is not a retained fixed pattern, a built-in or a VibeLang value: a construct signature givessubject instanceof C, then the draft's extraction; call signatures only call it as the draft does,C(subject, { matchType })with its receiver kept, and require abooleanreturn for a bare pattern andIterable<unknown> | falsefor an extractor; any other return type, or a callee with neither signature (any,unknown,Function), isEK0702. A type predicate narrows the subject asif (p(s))does. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 94). -
Amended 2026-10-29: an extractor on an ordinary class without its own matcher is
EK0704. When a class pattern's callee takes the construct-signature path (no ownSymbol.customMatcher, not a retained fixed pattern, built-in or VibeLang declaration), the draft's fallback matcher returnstrue, sowhen C(…), with any argument count,()included, can only fail to match or throwTypeError. It isEK0704, with the quick fixeswhen C:and, when the arguments bind the constructor's parameter properties,when C and { p1: let a, p2: let b }:. An inherited matcher doesn't count. The barewhen C:stayss instanceof C, and the run-time rule stays the draft's. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 100). -
Amended 2026-10-29: exhaustiveness for unions of objects with a literal
_tag. A subject member whose type has a_tagproperty of exactly one string literal type (an object type, an interface or a class instance type) is a checked member, soEK4701applies when every member is a checked kind; any member without a literal_tagstill turns the check off. Coverage is unchanged. Only_tagis a discriminant. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 102). -
Amended 2026-10-29: an
errorclass pattern narrows asEffect.catchTagdoes.when NotFound(let e):andwhen NotFound:narrow to the subject's own members whose_tagis"NotFound"when it has any; only otherwise (unknown,any, a non-union object type) does the binding take the declared class type. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 103). -
Amended 2026-10-30: leaving a
match. However amatchoristest ends, its cached iterators close before control goes on, as at the end of afor…ofbody: a value, areturn, abreakor acontinuecloses and goes on, and an error from closing replaces it. A pending throw with a closing error gives the draft'sAggregateError. The draft'sFinishMatchappends every abrupt completion to its errors, which turnsreturn 7intothrow 7; VibeLang reads that step as "a throw completion", as the draft's notes intend. In effect code a failedtry, or athrow, abandons the generator, so nothing closes. A no-match error isnew TypeError(). Areturnin anasync matchis a syntax error under its explicitly adopted VIP-7 body contexts. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later". -
Amended 2026-10-30:
_tagexhaustiveness counts literal cases.EK4701counts the literal cases ofswitch (s._tag)per normalized member. A pattern testing a shared literal covers each member's matching case even when TypeScript does not narrow the whole subject because every member has the same_tagtype. A generated error's bare or one-binding pattern likewise covers each member's matching tag case, including a plain object with that tag; any other tag cases on that member remain. A member is tagged when its_tagis a literal type: a string, number, bigint, boolean, enum,nullorundefinedliteral, aunique symbol, or a union of those. Each literal is a case, and an optional_taghas the caseundefined, so{ _tag?: "A" } | { _tag: "B" }is checked and the absent tag is left, astscleaves it. Any other_tagtype turns the check off. Members that share a tag are covered one at a time: an object pattern covers a member only when every other property it tests is required on that member, as"x" in snarrows. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later". This replaces "exactly one string literal type" in the amendment above. -
Amended 2026-10-30: leaving an
async matchisEK0706. Areturnthat belongs to anasync match, or abreakorcontinuewhose target is outside it, isEK0706, in the subject, patterns, guards and arm expressions, including inside adoexpression. A nested function keeps its ownreturn, and a jump to a target inside theasync matchfollows the ordinary rules. Ownership and targets come from the source, not from the generated async function. This codes the existing syntax error; it admits and forbids nothing new. -
Amended 2026-10-30: negation follows actual recognition.
not Puses only the TypeScript false-branch narrowing ofP's actual recognition test. Use!(s instanceof C)only for a dispatch path whose positive test iss instanceof C. Own matchers and predicates negate their invocation; an arbitrarybooleanresult narrows neither branch. Primitive/built-in adapters, retained Effect guards and generated tagged-error patterns likewise complement their actual tests. This adds no exhaustiveness coverage for negation.
Negation complements the actual recognition test for every dispatch path: literals use their equality test, ordinary classes their instanceof test, primitive/built-in adapters their specified tests, retained Effect patterns their module guards, generated tagged errors their tag tests, and own matchers or predicates their invocation. Only the false-branch narrowing established by that TypeScript test applies. An arbitrary boolean-returning matcher establishes no narrowing on either branch. This typing rule adds no exhaustiveness coverage for not.
// compiler-gap: pending VIP-7
class OnlyNumbers {
readonly kind = "instance"
static [Symbol.customMatcher](s: unknown, _hint: { matchType: "boolean" }): boolean {
return typeof s === "number"
}
}
function matcherComplement(s: OnlyNumbers | number) {
return match (s) { when not OnlyNumbers: s; default: "number"; }
}
// Replacing the negated arm result with s + 1 is TS2365: the matcher does not narrow s.
class Plain { readonly kind = "plain" }
function ordinaryComplement(s: Plain | number) {
return match (s) { when not Plain: s + 1; default: 0; }
}import { Effect, Exit } from "effect"
const complementMatcher: unique symbol =
((Symbol as typeof Symbol & { readonly customMatcher?: symbol }).customMatcher ?? Symbol.for("Symbol.customMatcher")) as any
class OnlyNumbers {
readonly kind = "instance"
static [complementMatcher](s: unknown, _hint: { matchType: "boolean" }): boolean {
return typeof s === "number"
}
}
function matcherComplement(s: OnlyNumbers | number) {
if (!OnlyNumbers[complementMatcher](s, { matchType: "boolean" })) return s
return "number"
}
function rejectedComplement(s: OnlyNumbers | number) {
if (!OnlyNumbers[complementMatcher](s, { matchType: "boolean" })) {
// @ts-expect-error TS2365: a boolean-returning matcher establishes no narrowing
return s + 1
}
return 0
}
class Plain { readonly kind = "plain" }
function ordinaryComplement(s: Plain | number) {
if (!(s instanceof Plain)) return s + 1
return 0
}
const instance = new OnlyNumbers()
const matcherExit = Effect.runSyncExit(Effect.sync(() => [
matcherComplement(instance) === instance,
matcherComplement(7),
ordinaryComplement(new Plain()),
ordinaryComplement(7)
]))
console.log(matcherExit) // Success([true, "number", 0, 8])- Amended 2026-10-30: async results and default flow use TypeScript's actual tests. For arm-type union
U,async matchhas typePromise<Awaited<U>>, with native async-return adoption and TypeScript's Promise-like/thenable checks. A returned Promise is adopted rather than nested. A default arm follows TypeScript control flow after the preceding ordered pattern and guard tests fail, including cached reads and source mutations. This is independent of exhaustiveness coverage: a predicate can narrow its false branch without covering a case, and a failed guard does not establish that its positive predicate failed. Do not subtract a coverage set instead of analyzing those tests.
// compiler-gap: pending VIP-7
function isText(s: string | number): s is string { return typeof s === "string" }
function classifyDefault(s: string | number) {
return match (s) { when isText: s.length; default: String(s); }
}
function guardedDefault(s: string | number) {
return match (s) {
when isText and if (false): 0;
default: typeof s === "string" ? s.length : s;
}
}
const adoptedResult: Promise<number> = async match (true) { default: Promise.resolve(7); }import { Effect, Exit } from "effect"
function isText(s: string | number): s is string { return typeof s === "string" }
function classifyDefault(s: string | number) {
if (isText(s)) return s.length
return String(s)
}
function guardedDefault(s: string | number) {
if (isText(s) && false) return 0
// @ts-expect-error the failed guard leaves both string and number possible
const numberOnly: number = s
return typeof s === "string" ? s.length : s
}
const adoptedResult: Promise<number> = (async () => Promise.resolve(7))()
const resultFlowExit = await Effect.runPromiseExit(Effect.gen(function* () {
return [
classifyDefault("text"),
classifyDefault(7),
guardedDefault("text"),
guardedDefault(7),
yield* Effect.promise(() => adoptedResult)
]
}))
console.log(resultFlowExit) // Success([4, "7", 4, 7, 7])-
Amended 2026-10-30: formal-parameter binding homes. An
istest that would introduce alet/constbinding into formal-parameter initialization isEK0705: the pinned explainer explicitly leaves that lexical home unfinished. This includes direct/destructured defaults and evaluated computed binding keys. Binding-free tests remain allowed; explicit source blocks and match arms inside an initializer keep their own local bindings, and nested source callable bodies keep their boundaries. Ordinary call arguments in a lexical block use that block. Preserve eager once-only initialization, parameter/body scope separation,length, TDZ, closures and exception boundaries; no generic move-to-body repair is supplied. This conservatively rejects an unspecified scope rather than introducing parameter-binding machinery. -
Amended 2026-10-31: limit and catch-parameter homes. A binding in a
for concurrentlimit belongs to the nearest home that encloses the loop, because the limit runs before the head environment exists. A binding in catch-clause parameter initialization isEK0705, as for a formal parameter: that environment is distinct from the catch Block, and no reconciliation gives it a binding home. Binding-free tests and explicit source blocks stay allowed. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 249). -
Amended 2026-10-30: remaining pattern-syntax diagnostics. Invalid
match/isgrammar and proposal-specific early errors without a more specific assigned diagnostic useEK0707. An interpolated primitive template pattern uses that code; write the ordinary interpolation in a guard. Do not duplicate a more specific diagnostic for the same rejection. Independent native lexical/type/name errors and errors in valid subexpressions keep TypeScript's diagnostics, including a malformed native template. Diagnostic ownership follows the source rule even when the parser reuses TypeScript helpers; this changes no admitted program. -
Amended 2026-10-30: generic and refined subject domains. Begin coverage normalization with the TypeScript type of the subject expression at its evaluation point, including represented flow refinements, and use its effective base constraint when one exists. Finite constrained parameters are checked conservatively over that constraint; unconstrained or broad parameters stay open. A represented intersection refinement may reduce the domain; a bare type parameter keeps its whole constraint even after a guard. Preserve literal, Boolean, enum and tag-case identity rather than boxing every subject with apparent-type conversion. This changes neither runtime tests nor ordinary arm/default-flow types.
-
Amended 2026-10-31: a bare fixed pattern covers its case.
when Option.Some:coversSome, aswhen Option.Some(let v):does, and likewise bareOption.None,Exit.Success,Exit.Failure,Result.SuccessandResult.Failure: the extracted-length rule applies only to a written argument list, and no list is not an empty list. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 78). -
Amended 2026-10-30: explicit async-match body contexts. The accepted Promise-producing sketch uses
[~Yield, +Await, ~Return, ~Effect]for its subject, tests and arms.awaitis allowed; a thrown exception rejects the Promise;returnand outwardbreak/continueare syntax errors. A nested source callable retains its own contexts. These are the snapshot's explicit reconciliations; its vendored specification has no formal async-match production to supply them.
Binding homes and redeclarations
The following rules specify binding homes under the adopted proposal.
Two top-level patterns that bind one name in the same lexical home are a redeclaration, as two let declarations are, and report what such a let reports: TS2451 beside another lexical declaration, TS2300 beside a parameter.
The statement body of if, else, for, for…in, for…of, for await, for concurrent, for try, while, do…while or a labeled statement, when it is not a Block, cannot be the lexical home of an is pattern binding: report EK0705, matching TypeScript's TS1156 for a let declaration in that position. The quick fix and vibelang --fix wrap that statement body in braces. An if head belongs to the home that contains the if statement; an else if head therefore needs braces around the else body when it binds. A binding inside an explicit do block, match arm, callable body or loop head keeps its own home.
A binding in a for concurrent limit belongs to the nearest home that encloses the loop, because the limit runs before the head environment exists; it is one cell, not copied into items.
An is test in formal-parameter initialization is EK0705 if a let or const pattern binding would need the parameter environment as its lexical home. This includes direct defaults, defaults nested in parameter destructuring, and evaluated computed binding keys. The same holds for a catch clause's parameter, whose environment is distinct from the catch Block. The pinned explainer leaves that scope unfinished, so no parameter-home binding is invented or moved into the body.
Meaning in Effect TS terms
match is plain JavaScript, not an Effect: TC39's semantics, with the arms tested in order and the subject evaluated once. It compiles to ordered tests on the subject, with no Effect involved.
Corrected 2026-10-29: the example ends every match arm with ;, the last one included, as the pinned TC39 grammar requires; a newline is not a separator (found by the round-3 review).
History
| Problem | Kind | Status |
|---|---|---|
Structural error types (D501) made instanceof arms unsound for exhaustiveness | lego, major | fixed (D515), kept here by custom matchers |
The subject's static type chose tag test vs instanceof | lego, major | fixed (D531), kept here by custom matchers |
or patterns: chapters disagreed on what runs | local, minor | fixed (D558), now TC39's definition |
| Guard grammar disagreed between grammar, chapters and parser | local, minor | fixed (D502), TC39's and if |
A local named Option still compiled to Option.isSome (D208) | local, none raised | open until Effect ships matchers |
The formatter broke a statement match after a call (ASI) | implementation | fixed |
No reviewer raised a match issue in round 11.
Prior art
The TC39 pattern-matching proposal; Effect's Match.exhaustive; Rust's and Swift's exhaustive match and switch.
Will's words
"Ok I accept it."
Footnote: old VibeLang
Old VibeLang is a reference only; nothing in it is part of the language unless a VIP adds it. For implementers, this VIP replaces:
D207's subset. D206, D209, D515 and D531 are restated here on TC39's terms.
- Amended 2026-11-01: project options and extraction presence. Pattern bindings have their specified TypeScript counterpart's types under the project's options; the strict examples do not freeze those types under
exactOptionalPropertyTypesorstrictNullChecks. Proven element/key presence preventsnoUncheckedIndexedAccessfrom adding artificialundefined; it does not remove actual payloadundefined. Pattern keys are not dotted reads undernoPropertyAccessFromIndexSignature. Written indexed and dotted reads keep those options. A printed counterpart carries extraction presence in its types, for example with a checked iterator result; a raw array-length guard alone is not that proof.