match Expression (and Statement)
The match expression provides structured pattern matching.
Key ideas:
- A
matchselects one of several branches based on a scrutinee expression. - The full language design includes richer patterns and arm guards, but the
current shipped subset documented here does not implement
ifguards in anymatchform yet. matchis an expression; all arms must be compatible in type.
The compiler must:
- Enforce exhaustiveness rules (where specified).
- Type check each arm and compute a consistent result type.
Surface Syntax#
The full language design includes rich pattern matching, guards, and matching over many scrutinee types. The current compiler implementation supports only a narrow, explicitly documented subset so we can validate end-to-end lowering and code generation.
In the initial subset, match is accepted as an expression of the form:
match <scrutinee> {
<pattern> => <expr>,
<pattern> => <expr>,
}
Notes:
- Arms are separated by commas; a trailing comma is permitted.
- In the initial subset, arm bodies are expressions (not blocks).
- In the current compiler, expression-form
matchis implemented for: - optionals,
- primitive integers,
- primitive booleans,
- primitive strings,
- enums,
- type unions,
- and recoverable
Result-style values. - Guard clauses of the form
pattern if cond => ...are currently unsupported across all of those subsets.
Optional Matching (T?)#
For optionals, the Supported forms is:
- The scrutinee expression must have optional type
T?(Option(T)), whereTis a payload type supported by the backend. - Patterns are restricted to:
NoneSome(<name>)Some(_)- No guards (
if ...) are implemented yet. - Matches must be exhaustive for the optional scrutinee: there must be exactly
one
Nonearm and exactly oneSome(...)arm (order is not significant).
Example:
fn main () -> int {
let x: int? = Some(7);
let y: int = match x {
None => 5,
Some(v) => v,
};
return y;
}
Integer Matching (Primitive Integers)#
The compiler also supports a small match subset for integer-like primitive
scrutinees.
Notes:
- The scrutinee expression must have a primitive integer type in the current
backend subset (
int,i64,u64, and the fixed-width integer primitives). - Patterns are restricted to:
- integer literals (
0,1,123,0xFF, ...), and - a wildcard
_arm. - The match must be exhaustive:
- when the scrutinee is not a known integer literal, there must be exactly one
wildcard
_arm, and that arm must be final, - when the scrutinee is a known integer literal, the wildcard may be omitted only if one integer-literal arm covers that exact value, and
- literal arms must not repeat the same value.
- No guards (
if ...) are implemented yet.
Example:
fn main () -> int {
let x: int = 0;
let y: int = match (x) {
0 => 1,
_ => 2,
};
return y;
}
Literal scrutinees may be exhaustively matched by the literal arms alone:
fn main () -> int {
let y: int = match 2 {
1 => 10,
2 => 20,
};
return y;
}
Boolean Matching (Primitive bool)#
The compiler also supports match over primitive bool scrutinees.
Supported forms:
- The scrutinee expression must have type
bool. - Patterns are restricted to:
- boolean literals (
falseandtrue), and - a wildcard
_arm. - The match must be exhaustive:
- when the scrutinee is not a known boolean literal, it must cover both
falseandtrue, or have exactly one final wildcard_arm, - when the scrutinee is a known boolean literal, the wildcard may be omitted only if one boolean-literal arm covers that exact value, and
- literal arms must not repeat the same boolean value.
- No guards (
if ...) are implemented yet.
Example:
fn main () -> int {
let b: bool = false;
let y: int = match b {
false => 1,
true => 2,
};
return y;
}
String Matching (Primitive string)#
The compiler also supports match over primitive string scrutinees.
Supported forms:
- The scrutinee expression must have type
string. - Patterns are restricted to:
- string literals (
"..."and raw backtick strings), and - a wildcard
_arm. - Literal comparison uses the decoded byte sequence of each string literal.
- The match must be exhaustive:
- when the scrutinee is not a known string literal, it must have exactly one
final wildcard
_arm, - when the scrutinee is a known string literal, the wildcard may be omitted only if one string-literal arm covers that exact decoded byte sequence, and
- literal arms must not repeat the same decoded byte sequence.
- No guards (
if ...) are implemented yet.
Example:
fn main () -> int {
let s: string = "foo";
let y: int = match s {
"foo" => 1,
_ => 2,
};
return y;
}
Literal scrutinees may be exhaustively matched by the literal arms alone:
fn main () -> int {
let y: int = match "foo" {
"foo" => 1,
"bar" => 2,
};
return y;
}
Enum Matching (enum)#
The language design supports matching over user-defined enum types
(enums).
Notes:
- The scrutinee expression must have an enum type
E(including an instantiated generic enum in module-set builds). - Patterns are restricted to enum variants:
- unit variants:
E::CancelledorCancelled - tuple variants:
E::Msg(x)/Msg(x)/E::Pair(a, b)/Pair(a, b)(binders may be identifiers or_) - For instantiated generic enums, the qualifier
Ein patterns may be a type alias for the instantiation (for exampletype R = Result(int, string);thenR::Ok(v)/R::Err(e)), or patterns may omit the qualifier and use the variant name directly. - No guards (
if ...) are implemented yet. - In expression form, enum matches must still be exhaustive:
- either there is exactly one explicit arm for each enum variant,
- or a final wildcard
_arm covers every remaining unmatched variant, - and if
_is used, it may appear at most once and must be the final arm.
Type Union and Concrete Typed-Binder Matching#
The language supports matching over type unions (type unions). The same typed-binder arm syntax is also accepted for concrete struct values in the backend when the checker can select exactly one applicable arm statically.
Notes:
- For a union scrutinee, the scrutinee expression must have a union type
T1 | ... | Tn. - For a concrete struct scrutinee, the scrutinee expression must have a known concrete struct type, and exactly one typed-binder arm must accept that type.
- Patterns are restricted to typed binders:
name: Ti(binds the payload asTi), or_: Ti(matches and ignores the payload), whereTiis one of the union member types for union scrutinees, or an accepting nominal type for concrete scrutinees.- For concrete struct scrutinees, an arm type accepts the scrutinee when it is:
- the exact concrete type or an alias of it,
- a valid base type in the struct
extendschain, or - an interface implemented by the concrete type.
- No guards (
if ...) are implemented yet. - Union matches must be exhaustive: there must be exactly one arm per union member type (order is not significant).
- Concrete typed-binder matches are statically selected: there must be exactly one accepting arm. Zero accepting arms are rejected as a missing arm, and more than one accepting arm is rejected as an ambiguous typed-binder match.
Example:
interface Object {}
struct A { x: int }
struct B { x: int }
impl A as Object {}
fn main () -> int {
let a = A { x: 1 };
let y = match a {
v: A => v.x,
_: B => 0,
};
let z = match a {
_: Object => 2,
};
return y + z;
}
Semantics#
- The scrutinee expression is evaluated exactly once.
- The selected arm is chosen based on the scrutinee value; non-selected arms are not evaluated.
- For
Some(v) => ..., the bindervis in scope only within that arm and has typeT(the inner payload type of the scrutineeT?). - For typed-binder arms
v: T => ..., the bindervis in scope only within that arm and has the annotated typeT;_: Tchecks the same type contract without introducing a binder. - The result type of a
matchexpression is the common type of its arms; all arms must type-check to the same result type in the initial subset. - Pattern binders for ownership-tracked payloads are owners, not borrowed aliases. When an arm consumes such a binder as its result, including by wrapping it in another enum or optional constructor, ownership transfers to the result and the selected payload in the scrutinee becomes uninitialized. Cleanup therefore destroys the returned owner exactly once and does not destroy the transferred source payload. Arms that do not consume their binder leave the selected payload owned by the scrutinee.
- A tuple variant may contain multiple ownership-tracked payloads. If an arm
transfers only some payload binders, the selected arm destroys every
unconsumed payload before invalidating the source variant; transferred
payloads remain live only in the arm result. An
_payload binder is unconsumed and follows the same destruction rule. - This path-sensitive transfer rule applies uniformly to user enums,
Some(T), and theOk(T)/Err(E)payloads of recoverable results. It is preserved across nested compositions such as an enum payload returned as an optional or an optional payload returned as another enum.
match Statement (Block Arms)#
Silk also supports a statement form of match whose arms are blocks. This is
the ergonomic counterpart to the expression form when an arm must perform
multiple statements (printing, early returns, mutation, etc).
Surface form:
match (<scrutinee>) {
<pattern> => { ... },
<pattern> => { ... },
}
The statement form may also use a single-expression arm body without braces:
match (x) {
_ => do_work(),
}
An optional trailing semicolon is permitted after the closing brace:
match (x) { _ => { } };
In Silk currently, the statement form is supported for:
- ordinary value matching (no typed-error contract), and
- typed error handling (typed errors).
Ordinary value matching#
When the scrutinee expression is an ordinary value (it does not have a typed
error contract), the statement form supports the same scrutinee + pattern
subsets as the match expression form in this document:
- optionals (
T?):None/Some(name)/Some(_) - primitive integers: integer literals and
_ - primitive booleans:
false/trueand_ - primitive strings: string literals and
_ - enums (
enum): enum variants (see note below) - type unions (
T1 | ... | Tn): typed bindersname: Ti/_: Ti - recoverable results:
Ok(name)/Ok(_)andErr(name)/Err(_)
Exhaustiveness rules:
- Expression
matchremains exhaustive. - Statement
matchis also exhaustive by default. - For
Option(T)and recoverableResult-like values only, the statement form may omit one side of the split: match (opt) { Some(v) => { ... } }match (opt) { None => { ... } }match (res) { Ok(v) => { ... } }match (res) { Err(e) => { ... } }- In that one-arm statement form, the unhandled case is an implicit no-op.
- This partial form does not apply to expression
match, integer matches, general enums, type unions, or typed-error matches.
Notes:
- The preferred single-branch control-flow forms remain
if let,let ... else, andwhile letwhen they fit naturally. - For ordinary enum matches, both expression and statement form now support one
final wildcard
_arm end to end. - That wildcard arm:
- may appear at most once,
- must be the final arm,
- and must cover at least one still-unmatched variant.
Enum variant pattern note (statement form):
- In the statement form, a bare identifier pattern
nameis reserved for a catch-all binder arm (used by typed error matches), so enum variant patterns must be written in qualified form:E::Variant(...)(including::pkg::E::Variant(...)). match (stream) { Error => { ... } }therefore treatsErroras a binder arm, whilematch (stream) { IOStream::Error => { ... } }matches the unit enum variant.- Ordinary enum statement matches now also allow one final
_catch-all arm, with the same final-arm and non-redundancy rules as expression-form enum matches.
Typed error matching (Terminal Arm Rule)#
The language design also includes a statement form of match used for
typed errors (typed errors).
Surface form:
match (expr) {
pattern => { ... },
err: SomeError => { std::abort(); }
}
Key semantic rule (Terminal Arm Rule):
- If
expris an error-producing expression (its signature includesT | ErrorType...), then any arm that matches anerrortype must end in a terminal statement.
Implementation
- The compiler currently implements
matchas an expression for the documented Supported forms: - optionals (
T?), - primitive integers,
- type unions,
- recoverable
Result-style values, - and exhaustive enum matches.
- No match arm guards (
pattern if cond => ...) are implemented yet in either expression or statement form. - The statement form is implemented for:
- ordinary values in the supported subset (block arms), and
- typed errors as part of the typed errors feature work (typed errors).
Note: the compiler also allows the match statement form to destructure
recoverable Result-style values. This form does not trigger the Terminal Arm
Rule because it is not a T | ... typed-error expression.
Result Matching (Ok(...) / Err(...))#
The match expression also supports a small subset for
recoverable “success or error” values. In the initial subset, this includes:
std::result::Result(T, E)(anenumwithOk(T)andErr(E)variants), and- “Result-like” structs of the form
{ value: T?, err: E? }.
For the struct form, the runtime invariant is: exactly one of value and err
is Some(...). If this invariant is broken at runtime, execution traps.
Patterns:
Ok(name)/Ok(_)Err(name)/Err(_)
Rules (Supported forms):
- Enum form:
- The scrutinee expression must have an enum type with variants
OkandErr. Ok(...)/Err(...)patterns are shorthand forR::Ok(...)/R::Err(...)whereRis the scrutinee enum type, and may appear alongside other enum variant patterns.- In expression form, exhaustiveness follows the enum rules: there must be exactly one arm per enum variant.
- Struct form:
- The scrutinee expression must have a nominal struct type that contains
value: T?anderr: E?. - Matches must be exhaustive:
- for enum scrutinees in expression form, follow the enum rules (one arm per variant, or a single final wildcard arm covers the remaining unmatched variant),
- for struct scrutinees, there must be exactly one
Ok(...)arm and exactly oneErr(...)arm. - In
Ok(v) => ..., the bindervhas typeT. - In
Err(e) => ..., the binderehas typeE.
Example:
import std::result;
import std::strings::String;
fn main () -> int {
let s: String = match String.from_string("hello") {
Ok(v) => v,
Err(_) => String.empty(),
};
return s.len as int;
}
One-arm statement examples:
match (parse_port(input)) {
Ok(port) => {
use_port(port);
},
}
match (std::env::get("HOME")) {
None => {
std::io::println("HOME is not set");
},
}
Source repository · Edit this page · View Markdown