Pattern matching#
Use match to handle the cases of an alternative type. Each branch can give names to the data carried by its case. For example, a payment may be paid, declined, or pending, and each outcome needs different handling.
A match accepts tagged alternative types, including Optional and Result, and operation contract views. Matching numeric or Boolean literals, guards, ranges, nested case patterns, and combined patterns is rejected. match is an expression. It can initialize a binding, supply an argument, or appear on its own with its result discarded. The scrutinee evaluates once, and only the selected arm runs.
The following function handles the Payment type declared in alternatives
:
fn describe(payment: Payment) -> String:
match payment:
Paid(receipt):
return "Paid: {receipt}"
Declined(reason):
return "Declined: {reason}"
Pending(reference):
return "Pending: {reference}"Keep match flow flat#
Nested match is an anti-pattern: another match inside a scrutinee or arm makes the control flow harder to follow. Bind the outer payload with an early exit, propagate compatible errors with try, use the ordinary Optional methods, or call a small helper for further case handling. The bounded TOML example
and the contract handler below show flat flow and a helper, respectively.
ms lint reports advisory MSL107 at each inner match, including matches inside an inline callback or its default expressions beneath another match. Separate helpers and sequential matches do not trigger it. Checking and execution still accept nested matches; the advisory is a style rule and adds no grammar restriction.
Arm values#
An arm may contain statements followed by a final expression. That expression supplies the arm’s value. An arm ending in an ordinary statement produces Unit when it reaches the end. All arms that produce values must have a compatible type; an annotation or expected argument/return type supplies context. Without context, independently typed value-producing arms establish the result type, including for earlier contextual values such as none or []. An inferred function result requires all arguments; it does not inherit omission permissions from one selected callable. Arms that exit through return, break, continue, or a Never expression do not contribute a result value.
fn amount(text: String) -> Result[Int, String]:
let cents = match text.parse_int():
some(value):
value
none:
return error(cause: "Invalid amount")
return ok(value: cents)This fragment uses the standard Optional and Result imports. return exits the enclosing function, including from a match used inside another expression; lexical cleanup still runs. break and continue target the enclosing loop. A nested anonymous function retains its own return boundary.
Short arms may place the value after the colon, such as none: 0. For an arm used only for effects, finish with unit when its last call returns another type. When a match stands on its own as a statement its result is discarded, so its arms need not agree on a type; when the result is used (a binding, argument, or return), every arm must produce that type. An empty arm is written unit; pass is not a keyword.
Payload patterns#
Case payloads bind positions in the declaration’s field order. A pattern must give exactly one identifier or _ placeholder per payload field. Pattern payloads are positional even though constructors always use named fields. Nullary cases use a bare case name without ().
type Position:
case Point(x: Int, y: Int)
case Unknown
fn horizontal(position: Position) -> Int:
match position:
Position.Point(horizontal_value, _):
return horizontal_value
Position.Unknown:
return 0Point(x: local, y: other): is rejected at parsing. Point(x): is rejected for wrong arity. Repeated binding identifiers are rejected, while _ can occur repeatedly because it creates no name. Each arm’s payload bindings and body share a lexical scope nested inside the enclosing scope.
An unqualified case in a pattern resolves within the scrutinee’s alternative type. A qualified case must belong to that exact type. Other.Point(...) cannot match Position, even when its payload types have the same shape. Constructor-expression name lookup instead follows alternative construction
.
Payload identifiers always create fresh bindings. For example, error(NotFound)
matches every error payload, even when NotFound is a case of that payload’s
type. It does not test for that case. Bind error(cause) and pass cause to a
helper that matches its cases, including NotFound: and the remaining cases.
A case that carries data needs its own payload names or _ placeholders in the
helper’s match. Keep matches flat; nested matches are an anti-pattern.
ms lint reports MSL106 at a capitalized payload identifier that coincides with
a known case or type name. The editor publishes the same advisory as a warning;
ms check still accepts the binding. If binding the whole payload is intentional,
use a name such as cause or _cause. Capitalized names without a known case or
type identity are allowed, and an enclosing value binding or generic parameter
with that name takes precedence when deciding whether to warn.
Exhaustiveness#
Match cases bind data without fallthrough; missing alternatives are compile errors. Wildcard cases must be explicit.
Every case in the example returns its own description. Running the Paid branch does not continue into Declined or Pending. The compiler checks that all alternatives are accounted for.
_: is the wildcard arm. It is permitted once, must be last, binds nothing, and covers every case not already handled. Duplicate cases, unknown cases, an arm after the wildcard, and a wildcard after all cases have been explicitly covered are rejected as invalid or unreachable arms. Qualified and unqualified spellings of the same case count as duplicates.
fn paid(payment: Payment) -> Bool:
match payment:
Paid(_):
return true
_:
return falseAdding a case to an alternative invalidates exhaustive matches that lack a wildcard. A match with a wildcard remains exhaustive; its wildcard handles the new case. Return completeness is checked across all reachable arms using the function return rules .
Optional and result values#
Optional and Result are also alternative types. Matching an optional distinguishes a present value from absence. Matching a result distinguishes success from an expected failure and lets the caller decide what to do with each.
Contract type patterns#
A contract view keeps its concrete receiver. Match that view with ConcreteType(receiver) to handle a particular implementation, followed by _: for generic handling. This is a checked recovery of an existing receiver, not a constructor call.
import JsonError from "@std/json.ms"
fn describe(cause: Error) -> String:
match cause:
JsonError(json): return describe_json(json)
_:
return cause.message()
fn describe_json(cause: JsonError) -> String:
match cause:
InvalidInput: return "Invalid JSON limits"
InvalidData: return "Malformed JSON"
LimitExceeded: return "JSON limit exceeded"The type must be an imported or local nominal record or alternative with an applicable explicit implementation of the scrutinee’s exact contract. Supply exactly one receiver name or _, including for a payloadless alternative. The binding has the concrete type, so its fields, cases, and implemented operations are available inside the arm. Native opaque receivers can be handled by the wildcard, but cannot be named in a concrete type pattern.
Generic patterns name the complete application, such as MyError[Int](cause); MyError[String] is a different pattern. An in-scope generic parameter may supply an argument, as in MyError[T](cause). Matching uses exact runtime nominal identities and type arguments, with the first matching arm selected. Duplicate statically identical type patterns are rejected. A pattern cannot name a different contract, a bare type parameter, or an individual alternative case.
A final wildcard is required even when every currently known implementation is listed: contracts are open to other implementations. It binds nothing; use the original contract value for generic handling. The scrutinee evaluates once; matching retains the same receiver reference and does not copy it, add cleanup, or catch faults and cancellation. See contract views .
Matching resource references#
Matching binds ordinary payload values and references. No binding is consumed, and matching does not register resource cleanup. Destructuring bindings
use brackets or braces; alternative match cases retain their Case(payload) syntax. See references inside other values
.