Language / match Expression (and Statement)

match Expression (and Statement)

The match expression provides structured pattern matching.

Key ideas:

  • A match selects 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 if guards in any match form yet.
  • match is 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 match is 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)), where T is a payload type supported by the backend.
  • Patterns are restricted to:
  • None
  • Some(<name>)
  • Some(_)
  • No guards (if ...) are implemented yet.
  • Matches must be exhaustive for the optional scrutinee: there must be exactly one None arm and exactly one Some(...) 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 (false and true), and
  • a wildcard _ arm.
  • The match must be exhaustive:
  • when the scrutinee is not a known boolean literal, it must cover both false and true, 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::Cancelled or Cancelled
  • 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 E in patterns may be a type alias for the instantiation (for example type R = Result(int, string); then R::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 as Ti), or
  • _: Ti (matches and ignores the payload), where Ti is 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 extends chain, 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 binder v is in scope only within that arm and has type T (the inner payload type of the scrutinee T?).
  • For typed-binder arms v: T => ..., the binder v is in scope only within that arm and has the annotated type T; _: T checks the same type contract without introducing a binder.
  • The result type of a match expression 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 the Ok(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 / true and _
  • primitive strings: string literals and _
  • enums (enum): enum variants (see note below)
  • type unions (T1 | ... | Tn): typed binders name: Ti / _: Ti
  • recoverable results: Ok(name) / Ok(_) and Err(name) / Err(_)

Exhaustiveness rules:

  • Expression match remains exhaustive.
  • Statement match is also exhaustive by default.
  • For Option(T) and recoverable Result-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, and while let when 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 name is 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 treats Error as a binder arm, while match (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 expr is an error-producing expression (its signature includes T | ErrorType...), then any arm that matches an error type must end in a terminal statement.

Implementation

  • The compiler currently implements match as 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) (an enum with Ok(T) and Err(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 Ok and Err.
  • Ok(...) / Err(...) patterns are shorthand for R::Ok(...) / R::Err(...) where R is 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? and err: 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 one Err(...) arm.
  • In Ok(v) => ..., the binder v has type T.
  • In Err(e) => ..., the binder e has type E.

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