Pattern Matching
match supports constant matching and union-type narrowing. It can also be used as an infix operator that returns bool.
Constant Matching
Integers, string, bool, and enum values can be matched by value:
let label = match status_code {
200 { "ok" }
201, 204 { "success" }
400...499 { "client error" }
else { "other" }
};
Labels in the same match body cannot overlap. An integer range a...b is closed and includes both endpoints.
An enum label must use the fully qualified enum case:
enum State {
Idle,
Running,
Done
}
match state {
State.Idle { println("idle"); }
State.Running { println("running"); }
State.Done { println("done"); }
}
Union Types
A union-form spec declares a set of possible member types:
spec Result: int | string;
let result: Result = "ready";
Before narrowing, a union value cannot access members directly or be compared for equality. Use a branch with a binding to obtain the concrete member:
let message = match result {
value: int { "code" }
text: string { text }
else { "unknown" }
};
A branch-head binding belongs to that branch's own header scope, and the following braced body is a child block. The body may declare a local binding with the same name and shadows the header binding from that declaration onward. Different branches may independently reuse one binding name.
A branch without a binding only tests the member type and does not change the original variable's static type:
match result {
int { println("integer"); }
string { println("text"); }
}
Infix match
value match pattern returns bool. Separate multiple labels with |:
if code match 200 | 201 | 204 {
println("success");
}
if score match 0...59 {
println("retry");
}
A union member pattern can bind the narrowed value at the same time:
if result match text: string && !text.isEmpty() {
println(text);
}
A binding is visible only where a successful match can be guaranteed statically. && carries a binding from its left
side into its right side and branch body; ||, !, and else do not propagate bindings. A binding propagated into an
if or while belongs to the condition-header scope, and the braced body is a child block that may shadow it. Multiple
bindings propagated by one && condition must use distinct names.
Expression Results
When match is an expression, every normally completing path must reach the final result expression of its branch.
A path that uses return to leave the current function, method, or lambda, or escapes through throw, produces no result and
does not participate in result-type checking. The expression form always requires an else branch, even when all union
members are listed. A standalone match statement may omit else. When the context
provides a target type, every normal result must fit that type. Without a contextual target, Feng determines a target
type from the normal results and requires the remaining normal results to fit it.
Nested Unions, Chained Patterns and Subset Bindings
Unions preserve their declared nesting. A -> B first matches direct member A, then its direct member B. A bound chained pattern obtains the final member value:
module manual_nested_union;
import std.io;
import std.numeric;
spec TextOrCode: int | string;
spec Reply: TextOrCode | bool;
spec Flat: int | string | bool;
/** Reads a nested member using its declared path. */
func describe(value: Reply): string {
return match value {
var code: TextOrCode -> int { code += 1; code.toString(); }
let text: TextOrCode -> string { text; }
bool { "flag"; }
else { "other"; }
};
}
/** Narrows a subset once more before using its concrete member. */
func describe_subset(value: Flat): string {
return match value {
let selected: int, string {
let text = match selected {
let code: int { code.toString(); }
let word: string { word; }
else { "other"; }
};
text;
}
bool { "flag"; }
else { "other"; }
};
}
/** Uses both an entered value and the first-member default. */
func main(args: string[]) {
let reply: Reply = 7;
let empty: Reply;
println("{0} {1} {2}", describe(reply), describe(empty), describe_subset("hello"));
}
The output is 8 1 hello. var code can be reassigned without changing the original union’s active member to a different type. Omitting the modifier or writing let creates an immutable binding. A comma-grouped branch still yields a union subset, which needs further narrowing before concrete member operations. Every chain edge must be a direct member edge; you cannot skip TextOrCode and match int directly.
A union without an initializer uses the default of its first direct member, recursively when that member is another union. Thus empty ultimately holds the integer zero. Expression-form match still requires else.
Union entry searches the entire member graph for an exact type first, then for a member that accepts conformance or target fitting. Each pass is breadth-first and selects the first match in declaration order at the same depth. A deep exact match therefore beats shallow contract conformance, and multiple valid paths alone are not ambiguous. To choose a nested member, explicitly form that member value before entering the outer union. Copying the same complete union type preserves its active member.
Object and Callable Contracts as Union Members
Form a callable value explicitly before entering a union to combine data and behavior in one result type. After narrowing, use the selected member’s capabilities:
module manual_union_contracts;
import std.io;
import std.numeric;
spec Named { func display(): string; }
spec Action(): string;
spec Result: Named | Action | int;
/** Supplies a nominal object-contract member. */
type Message: Named {
/** Returns the object member's text. */
func display(): string { return "object"; }
}
/** Uses the stored member rather than testing the runtime object type. */
func render(result: Result): string {
return match result {
let named: Named { named.display(); }
let action: Action { action(); }
let code: int { code.toString(); }
else { "other"; }
};
}
/** Creates both contract member forms. */
func main(args: string[]) {
let object: Result = Message {};
let callback: Action = () -> "callable";
let function: Result = callback;
println("{0} {1}", render(object), render(function));
}
The output is object callable. The Named branch matches only when the active member is Named. If the union also lists concrete Message and a value enters through that concrete member, its conformance does not make it match the Named branch. An object-contract view cannot then be narrowed downward to its runtime implementation type.