BAML match Expression (v3)
This page re-frames the match design in the Diátaxis style. The Reference section captures the normative specification: grammar, typing, evaluation, and diagnostics. The Explanation section records the rationale, constraints, and open questions that informed those choices.
This version (v3) incorporates the syntax decisions from the original proposal (v1), specifically using variable: Type for pattern matching.
Reference (facts & work support)
Summary
matchis an expression that evaluates to the value produced by the first arm whose pattern (and optional guard) succeeds.- Arms are evaluated top-to-bottom; later arms are unreachable if an earlier pattern covers the same space without a guard.
- Patterns can bind identifiers; each binding is scoped to its arm body.
- Exhaustiveness is enforced for closed types (enums, unions, literal unions, classes). Use
_or an unqualified binding pattern to opt in to “everything else.” returninside an arm exits the function, not just thematch. To produce a value from the arm, yield an expression (single expression or the last expression of a block).
Grammar (EBNF)
match-expression ::= "match" "(" expression ")" match-block
match-block ::= "{" match-arm+ "}"
match-arm ::= pattern guard? "=>" arm-body
guard ::= "if" expression // must be bool
arm-body ::= expression | block // block is `{ ... }`
pattern ::= wildcard-pattern
| binding-pattern
| typed-binding-pattern
| literal-pattern
| enum-variant-pattern
| destructuring-pattern
wildcard-pattern ::= "_" ( ":" type-name )?
binding-pattern ::= identifier
typed-binding-pattern ::= identifier ":" type-name
literal-pattern ::= literal
enum-variant-pattern ::= type-name "." identifier
destructuring-pattern ::= type-name? "{" field-patterns? "}"
field-patterns ::= field-pattern ( "," field-pattern )*
field-pattern ::= identifier ":" pattern
| identifier
| ".." // captures remaining fields
Pattern semantics
- Wildcard (
_or_: Type) matches any value (optionally constraining it to a type) and introduces no binding. - Binding (
name) matches any remaining value and binds it without narrowing its type. - Typed Binding (
name: Type) checks whether the candidate value is of the named type within a union and binds it toname. - Literal compares by value (numbers, strings, booleans).
- Enum variant matches a specific variant (e.g.,
Status.Active). - Destructuring matches structured types (classes/records). Each field pattern must succeed. Literal field values (e.g.,
User { name: "Admin" }) are sugar for a guardif name == "Admin"...keeps the remaining fields untouched but does not bind them. - Guards run only after the pattern matches; they must evaluate to
bool. If the guard isfalsethe arm is skipped and matching continues.
Typing and binding rules
- The scrutinee has static type
T. Each pattern is checked against the current residual type (what remains uncovered by previous arms). - Literal and enum patterns narrow to the exact value; the residual type removes that value from the set.
- Typed patterns
binding: Typecan only appear whenTypeis a constituent of the residual union. The bound identifier has typeType. - Binding patterns without a type qualifier (
name =>) do not narrow the type; the bound identifier keeps the residual type. - Destructuring patterns require the scrutinee to be (or contain) the referenced class/record type. Each field pattern is checked recursively; omitted fields are unconstrained.
- Guards cannot introduce new bindings; they can reference bindings from the pattern and outer scopes. Flow analysis uses successful guards when determining exhaustiveness of subsequent arms.
- Arm bodies must be type-compatible. The
matchexpression’s type is the least upper bound of all arm body types.
Exhaustiveness & reachability
- Closed sets (enums, finite literal unions) must be fully covered by explicit patterns or a catch-all (
_or unqualified binding). Missing cases produce an error listing uncovered variants/literals. - Union types require patterns that collectively cover every constituent.
_: Typeorbinding: Typearms count as covering that constituent; destructuring of a class covers that class variant. - Open types (plain
string,int,any, user-definedany) treat a final binding or_as sufficient coverage. - Guards affect coverage only when statically provable (e.g.,
User { age } if age < 18does not cover allUser, so later arms must handle the remainder). - If a pattern (taking guards into account) can never match because earlier arms already cover its space, the compiler emits an “unreachable pattern” diagnostic.
Evaluation semantics
- Evaluate the scrutinee expression once before matching.
- For each arm in order:
- Evaluate the pattern against the current value.
- If it matches, evaluate the guard (if present).
- On guard success, evaluate the arm body:
- Single-expression arms return that value.
- Block arms evaluate statements in order; the value of the last expression is the arm result.
return,break,continue, andthrowbehave exactly as they do outside ofmatch(i.e.,returnexits the enclosing function).
- The match expression yields the first arm result and stops; later arms are ignored.
- If no arm matches, emit a compile-time error (exhaustiveness should prevent this).
Diagnostics
- E0001 – Non-exhaustive match: reports missing literals/types/variants.
- E0002 – Unreachable pattern: earlier arms cover the same cases.
- E0003 – Invalid guard: guard is not boolean or references undefined bindings.
- E0004 – Invalid destructuring: field name does not exist or pattern type mismatch.
- E0005 – Ambiguous qualifier: type pattern refers to a name not present in the residual union.
- Diagnostics should quote the arm text and suggest adding
_ => ...when appropriate.
Reference examples
match (status) {
Status.Active => "Active"
Status.Inactive => "Inactive"
Status.Pending => "Pending"
}
match (input) {
"Special" => "special literal"
s: string => "plain string: " + s
}
// Assuming Result is a union of classes or similar structure
match (result) {
ok: ResultOk => handle_ok(ok.value)
err: ResultErr if err.retryable => retry(err)
err: ResultErr => fail(err)
}
match (user) {
User { name: "Admin" } => "Welcome, Administrator"
User { name, age } if age < 18 => "Hello, young " + name
User { name, .. } => "Hello, " + name
}
Explanation (concepts & rationale)
Goals
- Type safety: adding a new enum variant or union member must surface compile-time gaps.
- Expressiveness: LLM outputs are often polymorphic; pattern matching should make those flows concise.
- Consistency: Aligns with BAML’s expression-oriented functions and existing
scoped-catchsemantics.
Expression vs. statement
- Chosen as an expression to allow
let result = match (...)and concisereturn match (...). - Blocks evaluate to their last expression; explicit
returnremains available when exiting the entire function is clearer.
Pattern model
- Blends value patterns (for enums/literals) with type patterns (for unions) to match BAML’s mix of structural and nominal typing.
- Syntax Decision: Use
variable: Type(e.g.,s: string). - Rationale: Aligns with BAML’s variable declaration syntax (
let s: string = ...) and function arguments (arg: Type). It treats the pattern match as a "conditional declaration." - Discarded Alternative:
Type(variable)(Rust style) was considered but rejected because it resembles constructor calls, and primitives in BAML are not wrappers. - Binding patterns without qualifiers intentionally do not narrow; they exist for catch-alls and situations where the programmer wants the residual type wholesale.
Destructuring and guards
- Record/class patterns make nested data pipelines readable, mirroring Rust/Python structural matching.
- Literal fields desugar to guards so we maintain one mental model for conditional matching.
- Guards are evaluated after successful pattern binding to keep them pure “filters” rather than part of structural matching.
- Destructuring uses
Type { field: pattern }syntax, consistent with object construction.
Exhaustiveness strategy
- Mirrors Rust/TypeScript union checking: we treat enums, literal unions, and union types as closed worlds and require full coverage.
- Wildcards and unqualified bindings are the opt-out; they retain the residual type, which keeps later code honest about what might arrive.
- We intentionally do not try to reason about arbitrary guard predicates for coverage; only trivially exhaustive patterns (like
_) end the analysis.
Diagnostics & usability
- Early feedback on missing cases and unreachable arms ensures refactors surface immediately.
- Explicit error codes make it easier to document and test IDE diagnostics.
Open questions
- Pattern spreads: the reference grammar includes
..but does not yet specify binding remaining fields; decide whetherrestbindings are needed. - Nested
matchergonomics: consider sugar formatchinside pipeline expressions. - IDE representation: how should hover/quick-fix surfaces present missing-case suggestions for large unions?
Link the original exploratory notes (match-syntax.md) here once this spec is adopted, so readers can jump between the reference and the historical context.