The match expression lets you compare a value against a series of patterns and run different code for each case. It is similar to switch in other languages but more powerful: patterns can destructure structs, bind fields to local variables, and include if guards. Match is an expression — it returns a value, so you can assign the result or use it directly inside a larger expression.
Simple enum matching
The most common use: branch on an enum variant. Every variant must be covered or a _ wildcard must appear — for an ENUM the compiler checks exhaustiveness, because it knows the whole set of variants.
enum Direction { North, South, East, West }
A struct-enum: its variants carry named fields, which a match arm can destructure.
enum Shape {
Circle { radius: integer },
Rect { w: integer, h: integer },
}
A variant whose field is itself an enum.
enum Paint { Solid { rgb: integer }, Clear }
enum Tile { Painted { paint: Paint, n: integer }, Bare }
fn direction_name(d: Direction) -> text {
match d {
North => "north",
South => "south",
East => "east",
West => "west",
}
}
A guard reading the field its own pattern bound.
fn size_of(s: Shape) -> text {
match s {
Circle { radius } if radius > 10 => "big circle",
Circle { radius } => "small circle",
Rect { w, h } if w == h => "square",
Rect { w, h } => "rect",
}
}
fn main() {
assert(direction_name(North) == "north", "simple enum match");
assert(direction_name(West) == "west", "west");
Match as expression
Because match returns a value you can use it in assignments and arguments.
score = match Direction.East {
North | South => 10,
East | West => 20,
};
assert(score == 20, "match expression: {score}");
Struct-enum destructuring
When an enum has variants with fields (a struct-enum) you can bind the fields to variables in the match arm. Circle { radius } — binds the radius field to a local variable Rect { w, h } — binds both w and h In this short form the variable names must match the field names exactly; write Circle { radius: r } to bind the field under another name.
shape = Rect { w: 3, h: 4 };
shape_area = match shape {
Circle { radius } => radius * radius * 3,
Rect { w, h } => w * h,
};
assert(shape_area == 12, "struct-enum destructure: {shape_area}");
Patterns inside a field
A field can take a pattern instead of a name. A literal matches only that value, and when the field is itself an enum, a variant matches only that variant and binds its own fields in turn. A field the arm does not name is simply not tested. An arm with a pattern inside a field covers only part of its variant, so the match still needs _ (or an arm that binds the fields plainly) to cover the rest.
tile = Painted { paint: Solid { rgb: 255 }, n: 2 };
what = match tile {
Painted { paint: Clear, n } => "clear x{n}",
Painted { paint: Solid { rgb: 0 }, n } => "black x{n}",
Painted { paint: Solid { rgb }, n } => "colour {rgb} x{n}",
_ => "bare",
};
assert(what == "colour 255 x2", "a pattern inside a field: {what}");
Guards
Add if condition after a pattern to restrict when the arm matches. A guard can reject, so a guarded arm never counts towards covering an enum: a match whose arms name every variant but guard them all is still refused.
v = 42;
label = match v {
0 => "zero",
_ if v < 0 => "negative",
_ if v > 100 => "large",
_ => "normal",
};
assert(label == "normal", "guard: {label}");
A guard can also read what the PATTERN bound, which is where guards earn their keep — the field is in scope inside the condition.
assert(size_of(Circle { radius: 20 }) == "big circle", "guard on a binding");
assert(size_of(Circle { radius: 2 }) == "small circle", "guard on a binding, false");
assert(size_of(Rect { w: 3, h: 3 }) == "square", "guard comparing two bindings");
assert(size_of(Rect { w: 3, h: 4 }) == "rect", "guard comparing two bindings, false");
Wildcard _
The underscore _ matches anything, and it must be the LAST arm: it matches everything, so an arm written after it could never be selected, and the compiler says so.
Whether you NEED a _ depends on the subject. For an enum the compiler knows every variant, so it requires them all to be covered or a _ to be present, and refuses the match otherwise — naming the variants you left out. For an integer, character or text subject there is no finite set to check, so no _ is required and none is missed: a value that matches no arm makes the match answer null. That is the case to watch, because nothing warns you — see "When nothing matches" below.
x = 7;
kind = match x {
1 => "one",
2 => "two",
_ => "other",
};
assert(kind == "other", "wildcard: {kind}");
Integer matching
Match works on integers — each arm is compared with ==.
name = match 3 {
1 => "one",
2 => "two",
3 => "three",
_ => "many",
};
assert(name == "three", "integer match: {name}");
Range patterns
A numeric arm can match a whole inclusive range with low..=high — handy for grading, bucketing, or any "is it in this band" test.
grade = match 95 {
90..=100 => "A",
80..=89 => "B",
_ => "lower",
};
assert(grade == "A", "range pattern: {grade}");
Null patterns
When the subject is nullable, a null arm handles the absent case explicitly. With null-flow this is the idiomatic way to branch on a T? inside a match.
maybe: integer? = null;
presence = match maybe {
null => "absent",
0 => "zero",
_ => "present",
};
assert(presence == "absent", "null pattern: {presence}");
Text matching
Match also works on text values.
greeting = "hi";
reply = match greeting {
"hello" => "formal",
"hi" => "casual",
_ => "unknown",
};
assert(reply == "casual", "text match: {reply}");
Multiple patterns with |
Combine patterns with | to share the same arm body.
d = Direction.South;
axis = match d {
North | South => "vertical",
East | West => "horizontal",
};
assert(axis == "vertical", "multi-pattern: {axis}");
| joins variant NAMES. To bind fields in several variants with one arm, list the whole patterns separated by , instead: each pattern binds from its own variant, and the arm body runs for whichever one matched. A name every pattern binds must have the same type in each; a name only some patterns bind is nullable in the body, null when a pattern without it matched.
long_side = match Rect { w: 3, h: 8 } {
Circle { radius }, Rect { h: radius } => radius,
_ => 0,
};
assert(long_side == 8, "a field bound from either variant: {long_side}");
Tuple patterns
A tuple subject matches element by element. Write _ for an element you do not care about.
point = (2, "b");
where = match point {
(0, _) => "origin row",
(2, "a") => "two-a",
(2, "b") => "two-b",
_ => "elsewhere",
};
assert(where == "two-b", "tuple pattern: {where}");
An element can be an enum variant with its fields bound, so one arm reads both sides at once, and a guard can compare them.
a: Shape = Rect { w: 2, h: 3 };
b: Shape = Circle { radius: 5 };
bigger = match (a, b) {
(Rect { w, h }, Circle { radius }) if w * h > radius => "rect",
(Rect { w, h }, Circle { radius }) => "circle",
_ => "other",
};
assert(bigger == "rect", "variants inside a tuple pattern: {bigger}");
When nothing matches
This match names no _, and 7 is none of its arms. On an enum that would not compile; on a scalar there is no finite set to check, so the match answers null and the null travels on. Give the result a fallback with ??, or add a _ arm — the compiler will not remind you.
unmatched = match 7 {
1 => "one",
2 => "two",
};
assert(unmatched == null, "a scalar match that selects no arm answers null");
assert((unmatched ?? "none") == "none", "so give it a fallback");
Match inside an arm
Match is an expression, so it can appear inside other expressions, including another match's arm.
n = 15;
result = match n % 2 == 0 {
true => "even",
false => match n % 3 == 0 {
true => "odd multiple of 3",
false => "odd",
},
};
assert(result == "odd multiple of 3", "nested: {result}");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Match
// @TITLE: Pattern matching with match expressions
// The `match` expression lets you compare a value against a series of patterns
// and run different code for each case. It is similar to `switch` in other
// languages but more powerful: patterns can destructure structs, bind fields to
// local variables, and include `if` guards.
// Match is an expression — it returns a value, so you can assign the result
// or use it directly inside a larger expression.
// ## Simple enum matching
// The most common use: branch on an enum variant. Every variant must be covered
// or a `_` wildcard must appear — for an ENUM the compiler checks exhaustiveness,
// because it knows the whole set of variants.
enum Direction { North, South, East, West }
// A struct-enum: its variants carry named fields, which a match arm can destructure.
enum Shape {
Circle { radius: integer },
Rect { w: integer, h: integer },
}
// A variant whose field is itself an enum.
enum Paint { Solid { rgb: integer }, Clear }
enum Tile { Painted { paint: Paint, n: integer }, Bare }
fn direction_name(d: Direction) -> text {
match d {
North => "north",
South => "south",
East => "east",
West => "west",
}
}
// A guard reading the field its own pattern bound.
fn size_of(s: Shape) -> text {
match s {
Circle { radius } if radius > 10 => "big circle",
Circle { radius } => "small circle",
Rect { w, h } if w == h => "square",
Rect { w, h } => "rect",
}
}
fn main() {
assert(direction_name(North) == "north", "simple enum match");
assert(direction_name(West) == "west", "west");
// ## Match as expression
// Because match returns a value you can use it in assignments and arguments.
score = match Direction.East {
North | South => 10,
East | West => 20,
};
assert(score == 20, "match expression: {score}");
// ## Struct-enum destructuring
// When an enum has variants with fields (a struct-enum) you can bind the
// fields to variables in the match arm.
// Circle { radius } — binds the 'radius' field to a local variable
// Rect { w, h } — binds both 'w' and 'h'
// In this short form the variable names must match the field names exactly;
// write `Circle { radius: r }` to bind the field under another name.
shape = Rect { w: 3, h: 4 };
shape_area = match shape {
Circle { radius } => radius * radius * 3,
Rect { w, h } => w * h,
};
assert(shape_area == 12, "struct-enum destructure: {shape_area}");
// ## Patterns inside a field
// A field can take a pattern instead of a name. A literal matches only that
// value, and when the field is itself an enum, a variant matches only that
// variant and binds its own fields in turn. A field the arm does not name is
// simply not tested. An arm with a pattern inside a field covers only part of
// its variant, so the match still needs `_` (or an arm that binds the fields
// plainly) to cover the rest.
tile = Painted { paint: Solid { rgb: 255 }, n: 2 };
what = match tile {
Painted { paint: Clear, n } => "clear x{n}",
Painted { paint: Solid { rgb: 0 }, n } => "black x{n}",
Painted { paint: Solid { rgb }, n } => "colour {rgb} x{n}",
_ => "bare",
};
assert(what == "colour 255 x2", "a pattern inside a field: {what}");
// ## Guards
// Add `if condition` after a pattern to restrict when the arm matches.
// A guard can reject, so a guarded arm never counts towards covering an enum:
// a match whose arms name every variant but guard them all is still refused.
v = 42;
label = match v {
0 => "zero",
_ if v < 0 => "negative",
_ if v > 100 => "large",
_ => "normal",
};
assert(label == "normal", "guard: {label}");
// A guard can also read what the PATTERN bound, which is where guards earn
// their keep — the field is in scope inside the condition.
assert(size_of(Circle { radius: 20 }) == "big circle", "guard on a binding");
assert(size_of(Circle { radius: 2 }) == "small circle", "guard on a binding, false");
assert(size_of(Rect { w: 3, h: 3 }) == "square", "guard comparing two bindings");
assert(size_of(Rect { w: 3, h: 4 }) == "rect", "guard comparing two bindings, false");
// ## Wildcard `_`
// The underscore `_` matches anything, and it must be the LAST arm: it matches
// everything, so an arm written after it could never be selected, and the
// compiler says so.
//
// Whether you NEED a `_` depends on the subject. For an enum the compiler
// knows every variant, so it requires them all to be covered or a `_` to be
// present, and refuses the match otherwise — naming the variants you left out.
// For an integer, character or text subject there is no finite set to check,
// so no `_` is required and none is missed: a value that matches no arm makes
// the match answer null. That is the case to watch, because nothing warns you
// — see "When nothing matches" below.
x = 7;
kind = match x {
1 => "one",
2 => "two",
_ => "other",
};
assert(kind == "other", "wildcard: {kind}");
// ## Integer matching
// Match works on integers — each arm is compared with `==`.
name = match 3 {
1 => "one",
2 => "two",
3 => "three",
_ => "many",
};
assert(name == "three", "integer match: {name}");
// ## Range patterns
// A numeric arm can match a whole inclusive range with `low..=high` — handy for
// grading, bucketing, or any "is it in this band" test.
grade = match 95 {
90..=100 => "A",
80..=89 => "B",
_ => "lower",
};
assert(grade == "A", "range pattern: {grade}");
// ## Null patterns
// When the subject is nullable, a `null` arm handles the absent case explicitly.
// With null-flow this is the idiomatic way to branch on a `T?` inside a match.
maybe: integer? = null;
presence = match maybe {
null => "absent",
0 => "zero",
_ => "present",
};
assert(presence == "absent", "null pattern: {presence}");
// ## Text matching
// Match also works on text values.
greeting = "hi";
reply = match greeting {
"hello" => "formal",
"hi" => "casual",
_ => "unknown",
};
assert(reply == "casual", "text match: {reply}");
// ## Multiple patterns with `|`
// Combine patterns with `|` to share the same arm body.
d = Direction.South;
axis = match d {
North | South => "vertical",
East | West => "horizontal",
};
assert(axis == "vertical", "multi-pattern: {axis}");
// `|` joins variant NAMES. To bind fields in several variants with one arm, list the whole
// patterns separated by `,` instead: each pattern binds from its own variant, and the arm
// body runs for whichever one matched. A name every pattern binds must have the same type
// in each; a name only some patterns bind is nullable in the body, `null` when a pattern
// without it matched.
long_side = match Rect { w: 3, h: 8 } {
Circle { radius }, Rect { h: radius } => radius,
_ => 0,
};
assert(long_side == 8, "a field bound from either variant: {long_side}");
// ## Tuple patterns
// A tuple subject matches element by element. Write `_` for an element you
// do not care about.
point = (2, "b");
where = match point {
(0, _) => "origin row",
(2, "a") => "two-a",
(2, "b") => "two-b",
_ => "elsewhere",
};
assert(where == "two-b", "tuple pattern: {where}");
// An element can be an enum variant with its fields bound, so one arm reads both
// sides at once, and a guard can compare them.
a: Shape = Rect { w: 2, h: 3 };
b: Shape = Circle { radius: 5 };
bigger = match (a, b) {
(Rect { w, h }, Circle { radius }) if w * h > radius => "rect",
(Rect { w, h }, Circle { radius }) => "circle",
_ => "other",
};
assert(bigger == "rect", "variants inside a tuple pattern: {bigger}");
// ## When nothing matches
// This match names no `_`, and 7 is none of its arms. On an enum that would
// not compile; on a scalar there is no finite set to check, so the match
// answers null and the null travels on. Give the result a fallback with `??`,
// or add a `_` arm — the compiler will not remind you.
unmatched = match 7 {
1 => "one",
2 => "two",
};
assert(unmatched == null, "a scalar match that selects no arm answers null");
assert((unmatched ?? "none") == "none", "so give it a fallback");
// ## Match inside an arm
// Match is an expression, so it can appear inside other expressions, including
// another match's arm.
n = 15;
result = match n % 2 == 0 {
true => "even",
false => match n % 3 == 0 {
true => "odd multiple of 3",
false => "odd",
},
};
assert(result == "odd multiple of 3", "nested: {result}");
}