Skip to content
Topaz6.0 · Before release
Download

Search documentation

Specification 6.0 · Not released · Tested product 5.21.3

Use arrow keys to choose a result. Press Enter to open.

Type an API name, diagnostic number, rule, or project topic.

    Language · chapter 09 · 12 rules

    Pattern matching

    Defines patterns, matching, and accepted matches.

    Definition and scope

    Link

    A pattern describes a value to recognize and the names to bind from it. A match expression selects a case by applying a pattern and, when present, a guard.

    This page owns pattern syntax, case selection, and the checking rules stated below. The execution of if let and while let, including their branches and repetition, belongs to Blocks and control flow. Collection construction, nominal declarations, and general name resolution belong to their respective Language pages.

    This page defines source-order selection, exhaustive matches, and checking of unreachable cases. Each example states its expected result independently of recorded observations.

    Syntax

    Link
    EBNF
    1. Pattern = PrimaryPattern ("|" PrimaryPattern)* ;
    2. PrimaryPattern =
    3. "_" | Identifier | Literal | ConstructorPattern | ListPattern
    4. | RecordPattern | NominalRecordPattern | TypedPattern | RangePattern ;
    5. ConstructorPattern =
    6. Identifier "(" (Pattern ("," Pattern)* ","?)? ")" ;
    7. ListPattern = "[" (ListElement ("," ListElement)* ","?)? "]" ;
    8. ListElement = Pattern | ".." Pattern ;
    9. RecordPattern = "{" FieldPattern ("," FieldPattern)* ","? "}" ;
    10. FieldPattern = FieldName ":" Pattern ;
    11. NominalRecordPattern = Identifier RecordPattern ;
    12. TypedPattern = Identifier ":" Type ;
    13. RangePattern = "(" RangeEndpoint (".." | "..<") RangeEndpoint ")" ;

    This is a hand-transcribed source grammar excerpt. Type includes the type parser's own alternatives; the excerpt does not settle every interaction between a type annotation and an or-pattern. RangeEndpoint imports the expression subset accepted at a range-pattern boundary.

    The grammar defines pattern syntax. Scrutinee types, constructor arity, binding rules, and unresolved rest boundaries also affect valid programs.

    Source evidence
    EBNF
    1. MatchShape = "match" "(" Expression ")" "{" CaseShape+ "}" ;
    2. CaseShape = "case" Pattern ("if" "(" Expression ")")? "=>" CaseBody ;
    3. CaseBody = Expression ;

    The + requires at least one case. This shape excerpt omits case separators and expression/block disambiguation. A case body is an expression, which may be a block expression. Write a return statement inside that block. Its transfer and cleanup follow Blocks and control flow.

    Source evidence

    Pattern forms

    Link

    The table is a syntax index for the grammar excerpt. Its spellings are fragments, not separately executed programs. Constructor argument lists identify constructor patterns. A bare name attempts to bind, subject to the constructor-name collision rule.

    Pattern formRepresentative spelling
    Bindingname
    Wildcard_
    Literal0, true, "ready", ()
    ConstructorSome(item), None(), Empty(), Tag(payload)
    List[first, second], [head, ..tail]
    Rest within a list.._, ..tail
    Structural record{ x: x, y: other }
    Nominal recordPoint { x: x, y: y }
    Or-pattern1 \| 2
    Integer range(0 .. 9), (0 ..< 10)
    Typed bindingitem: int
    Ruletopaz.rule.pattern-matching.forms

    Pattern forms and name interpretation

    The pattern forms are binding, wildcard, literal, constructor, list, record, nominal-record, or, range, typed. Rest patterns appear in list patterns. A record pattern has at least 1 field. Each field uses field: pattern. A field without a colon and pattern is rejected with TPZ2001 during parsing.

    Every constructor pattern has an argument list. A constructor with no payload uses Name(), including None(). List-element bindings have the array element type, and a rest binding has an array type.

    Why

    A shorthand field and an explicit mapping to the same name both bind that field value under that name. Explicit field mappings preserve this intent and use the same syntax for renaming and nested patterns. Pattern bindings preserve each field type.

    Source evidence

    Use Name(...) to test a constructor and a fresh name or _ to capture or ignore the whole value. A bare constructor name is rejected instead of becoming a catch-all.

    Ruletopaz.rule.pattern-matching.constructor-spelling

    Constructor syntax and binding names

    Write Name(patterns) in every pattern position. The payload list has the declared arity. A zero-payload constructor uses Name(), including None() and user enum variants such as Empty(). Nesting does not change this syntax. Parenthesized patterns with the wrong payload count retain the existing constructor-arity diagnostic.

    Write Name(arguments) in every value position at every arity. Constructor calls use None(), Empty(), or Some(value). Qualified enum construction uses the same argument list, as in Choice.Empty(). The grammar rejects a bare prelude None value. User enum constructor names are resolved and checked after parsing.

    A bare identifier in a pattern attempts to bind a new name. Its capitalization never selects a constructor role. A typed binding uses the same name rule.

    The constructor namespace includes prelude constructors, visible declarations and imports, and variants obtained from the known expected type after resolving aliases. A pattern binder that has the same identifier as one of those constructors is rejected with TPZ5712. This check applies at every nested pattern, to typed binders, and to constructors of another visible type. A value binding does not hide a constructor for this check. Choose a fresh name to capture the value or use _ to ignore it.

    None is already a reserved binding name. Without the constructor argument list it is rejected during parsing with TPZ2012. Other bare constructor names reach the namespace check. A removed bare enum pattern is never widened into a catch-all.

    Why

    Bare and parenthesized nullary constructors denote the same variant without a payload. Parentheses preserve that construction or matching intent and identify constructors at every arity. A bare pattern name attempts to bind. Rejecting constructor-name collisions prevents a former tag test from silently becoming a catch-all. Capitalization, type annotations, nesting, and value shadowing do not change this rule.

    Source evidence

    Selecting a case

    Link
    Ruletopaz.rule.pattern-matching.selection

    Case selection and guards

    A match has at least one case and evaluates its scrutinee once. It tries cases in source order and tests the pattern of each case in turn. After a pattern succeeds it installs the bindings and evaluates the guard once if present. The guard has type bool. Bindings are available in the guard and body.

    An absent guard counts as true. A false guard continues with the next case, including a case with the same pattern. The first case whose pattern and guard succeed is selected. A catch-all serves as a fallback at its written position. The selected body produces the match value. Other bodies are not evaluated.

    Every match must satisfy the exhaustiveness rule during checking.

    An arm body is an expression. Write a return statement inside a block, as in case true => { return value }. A return without that block is rejected during parsing with TPZ2001.

    Every match follows the exhaustiveness rule.

    Why

    Evaluating the scrutinee once avoids repeating its effects. Source order makes the exceptional case explicit before a more general case handles the remaining inputs. A failed guard leaves later cases available. Rejecting cases that earlier unguarded patterns make unreachable prevents an arm from being silently hidden. A bare return in an arm and a block containing that return request the same exit from the enclosing function with the same value. The block expresses that transfer through ordinary statement rules while each arm has an expression body.

    Source evidence
    Ruletopaz.rule.pattern-matching.wildcard

    Wildcard pattern

    _ matches any scrutinee. It is irrefutable and introduces no bindings.

    Why

    Ignoring a value should not introduce a name that could hide another binding. The wildcard makes that intent explicit.

    Source evidence

    The selection example records scrutinee evaluation, guard visits, and body visits. The expected trace makes skipped guards and unselected bodies observable.

    Bindings and scalar patterns

    Link
    Ruletopaz.rule.pattern-matching.or-bindings

    Names in or-pattern alternatives

    Each alternative of an or-pattern must bind the same set of names. Different name sets are rejected during checking with TPZ5710. The TPZ5711 examples reject a shared name with int and string payloads and accept matching int payloads. The full binding-type compatibility algorithm across alternatives remains Pending.

    Why

    Every successful alternative must provide the names used by the shared guard and body. Equal binding sets prevent a name from being absent on one path.

    Source evidence
    Ruletopaz.rule.pattern-matching.typed-binding

    Typed binding patterns

    For a known scrutinee type that overlaps the annotation, a typed binding has the overlapping part of those two types. The annotation selects the matched type.

    Why

    The overlap identifies values that can satisfy both the scrutinee type and the annotation. It gives the binding a type valid on a successful match.

    Source evidence
    Ruletopaz.rule.pattern-matching.integer-range

    Integer range patterns

    An integer range pattern compares an int scrutinee with int endpoints. The value must be greater than or equal to the lower endpoint and satisfy the upper comparison. Both comparisons must succeed. The pattern introduces no bindings.

    Pattern operatorUpper comparison
    ..Less than or equal to the upper endpoint
    ..<Less than the upper endpoint

    Why

    Using the same endpoint operators as range expressions keeps boundary meaning consistent between matching and iteration.

    Source evidence
    Ruletopaz.rule.pattern-matching.single-rest

    A single list rest and its surrounding elements

    A pattern after the rest marker is required. Write ..name to bind the rest or .._ to ignore it. An omitted pattern is rejected during parsing with TPZ2001.

    A list pattern with one rest may put it before, between, or after fixed elements. The input array must contain at least as many elements as the fixed prefix and suffix together. Those elements are matched in order. The rest has type Array<T> and may be empty. Admission of multiple rest elements remains Pending. Their implementation limitation is documented separately.

    Why

    An omitted rest pattern and an explicit wildcard both discard the unmatched slice. Writing the wildcard preserves that intent and distinguishes it from binding the slice. One rest determines the slice between the fixed prefix and suffix.

    Source evidence
    Ruletopaz.rule.pattern-matching.range-delimiters

    Range pattern boundaries

    Write a range pattern as (lower .. upper) or (lower ..< upper). The outer parentheses belong to the range pattern and close before the case arrow. Endpoints use the ordinary additive-expression grammar and must have type int. An arm may produce a lambda without taking the case arrow into an endpoint. An unwrapped range pattern is rejected during parsing with TPZ2001.

    Why

    A bare range pattern and the same range inside parentheses test the same endpoints with the same inclusive or exclusive upper bound. The parentheses preserve that interval intent and give the pattern a closing boundary before the case arrow. This separates endpoint expressions from an arm that produces a lambda without changing expression parsing according to the endpoint spelling.

    Source evidence

    Accepted matches and coverage

    Link
    Ruletopaz.rule.pattern-matching.bool-coverage

    Coverage of a known bool

    The bool examples apply the exhaustiveness rule to true, false. The missing-case example reports TPZ5021.

    Why

    These examples show the closed-type coverage required by the general exhaustiveness rule.

    Source evidence

    The exhaustiveness rule applies to closed and open types, including nested payloads.

    Cases are tried in source order. A later case may overlap an earlier case when it still matches some reachable input. An unguarded wildcard or total binding selects at its written position. Earlier unguarded cases that cover every value a later pattern can match make that later case a checking error. Every arm is still type-checked.

    Ruletopaz.rule.pattern-matching.unreachable-cases

    Reachable overlapping cases

    Cases are tried in source order. A case may overlap an earlier case if it can match an input that reaches it. A specific case can therefore precede a general case. An unguarded wildcard or total binding is an ordinary case and acts as a fallback at its written position. There is no separate residual fallback selection.

    A later case is unreachable when the union of preceding unguarded patterns covers every value its pattern could match. This includes a case shadowed by a single catch-all or by several earlier cases. Pattern typing runs before this comparison. A guard cannot establish unconditional coverage because it may fail. Overlapping cases with reachable inputs remain valid even when their intersection is unknown.

    Alternatives of one or-pattern describe one case domain. Their overlap does not make a later case unreachable. Binding consistency is checked by its separate rule. All case bodies are checked even when their patterns are unreachable.

    An unreachable case is rejected during checking with TPZ5713. A typed pattern incompatible with the known scrutinee type still uses TPZ5001. Match completeness follows the exhaustiveness rule.

    Why

    An exceptional case can intentionally precede a general case. That use of overlap has a different intent from dividing a domain into mutually exclusive categories. Source order expresses which matching case takes priority. A failed guard lets later cases handle the input. Rejecting a case hidden by earlier unguarded patterns prevents code order from silently disabling a branch.

    Source evidence
    Ruletopaz.rule.pattern-matching.exhaustiveness

    Every match is exhaustive

    Every match must be exhaustive during checking. A closed type has cases that can all be enumerated. Enums, Result, Option, bool, and closed unions must cover every case. Coverage also examines closed types inside payloads. For example, Option<bool> is covered by Some(true), Some(false), and None().

    Open types include int, float, string, arrays, ranges, type parameters, and other types whose cases cannot be enumerated. A match over an open type ends with an unguarded wildcard or name pattern. A guarded arm does not contribute to coverage, even when its guard is a literal true.

    Missing coverage is rejected during checking with TPZ5021. TPZ4006 is retired in 6.0.0 and is replaced by TPZ5021. This rule does not change refutable let or for patterns.

    Case selection follows source order. TPZ5021 reports missing coverage. TPZ4006 records the retired runtime failure.

    Why

    Checking every match removes execution-time match failure. A program handles each input before it runs.

    Source evidence

    Examples and expected results

    Link

    Each example lists its expected result and the recorded public CLI observation.

    Observe the first successful case and guard

    Link
    example.tpzObserve the first successful case and guardOpen in Playground
    1. import std.io
    2. function readValue() -> int {
    3. io.writeLine("scrutinee")
    4. 2
    5. }
    6. function testCase(label: string, answer: bool) -> bool {
    7. io.writeLine(label)
    8. answer
    9. }
    10. function selectedBody(label: string) -> string {
    11. io.writeLine("body: {label}")
    12. label
    13. }
    14. let answer = match (readValue()) {
    15. case 1 if (testCase("unmatched guard", true)) => selectedBody("one")
    16. case 2 if (testCase("selected guard", true)) => selectedBody("selected")
    17. case _ => selectedBody("fallback")
    18. }
    19. io.writeLine(answer)
    Expected outputexit 0
    scrutinee
    selected guard
    body: selected
    selected
    
    $ topaz run example.tpz --compiler self
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. scrutinee
    2. selected guard
    3. body: selected
    4. selected

    The input selects one explicit pattern. Its guard and body run once. A guard belonging to another pattern is not evaluated. This revised expectation has no current product receipt.

    Source evidence

    Literal alternatives and integer range boundaries

    Link
    example.tpzLiteral alternatives and integer range boundariesOpen in Playground
    1. import std.io
    2. function classify(value: int) -> string {
    3. match (value) {
    4. case 1 | 2 => "choice"
    5. case (3..<6) => "inside"
    6. case (7..9) => "closed"
    7. case _ => "outside"
    8. }
    9. }
    10. io.writeLine(classify(1))
    11. io.writeLine(classify(2))
    12. io.writeLine(classify(3))
    13. io.writeLine(classify(5))
    14. io.writeLine(classify(6))
    15. io.writeLine(classify(9))
    16. io.writeLine(classify(10))
    Recorded outputexit 0
    choice
    choice
    inside
    inside
    outside
    closed
    outside
    
    $ topaz run example.tpz --compiler self · recorded with topaz 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. choice
    2. choice
    3. inside
    4. inside
    5. outside
    6. closed
    7. outside
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. choice
    2. choice
    3. inside
    4. inside
    5. outside
    6. closed
    7. outside

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation

    The expected values exercise both endpoints of the half-open interval and the upper endpoint of the closed interval. They do not establish coverage of an integer domain.

    Source evidence

    Check field, rest, typed and constructor bindings

    Link
    example.tpzCheck field, rest, typed and constructor bindingsOpen in Playground
    1. record Point {
    2. x: int,
    3. y: int,
    4. }
    5. function pointTotal(value: Point) -> int {
    6. match (value) {
    7. case Point { x: 0, y: other } => other
    8. case remaining => remaining.x + remaining.y
    9. }
    10. }
    11. function structuralTotal(value: { x: int, y: int }) -> int {
    12. match (value) {
    13. case { x: 0, y: other } => other
    14. case remaining => remaining.x + remaining.y
    15. }
    16. }
    17. function remainder(values: Array<int>) -> Array<int> {
    18. match (values) {
    19. case [_, ..tail] => tail
    20. case _ => []
    21. }
    22. }
    23. function integerPart(value: int | string) -> int {
    24. match (value) {
    25. case item: int => item
    26. case _ => 0
    27. }
    28. }
    29. function selected(value: Result<int, int>) -> int {
    30. match (value) {
    31. case Ok(item) | Err(item) => item
    32. }
    33. }
    Expected outputexit 0
    $ topaz check example.tpz --compiler self
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    This is a complete checking fixture. Explicit result types make the field, remainder and typed-binding uses visible to checking. The record declaration and collection literals provide context; their construction rules are owned by other Language pages.

    Source evidence

    A known bool with one missing case

    Link
    example.tpzA known bool with one missing caseOpen in Playground
    1. function flag(value: bool) -> int {
    2. match (value) {
    3. case true => 1
    4. }
    5. }
    Recorded outputexit 1
    No stdout.
    error[TPZ5021]: non-exhaustive match: missing `false`
     --> example.tpz:2:5
      |
    2 |     match value {
      |     ^^^^^^^^^^^^^
    
    example.tpz: 1 type diagnostic
    
    $ topaz check example.tpz --compiler self · recorded with topaz 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5021 missing-unguarded-false-case-for-known-bool at example.tpz:2:5
    Observed assertions · passed · 5.21.3

    Exit code 1.

    No stdout.

    Observed stderr
    1. error[TPZ5021]: non-exhaustive match: missing `false`
    2. --> example.tpz:2:5
    3. |
    4. 2 | match value {
    5. | ^^^^^^^^^^^^^
    6. example.tpz: 1 type diagnostic
    • Passed · expected nonzero; observed 1
    • Passed · checking TPZ5021; cause missing-unguarded-false-case-for-known-bool; primary location asserted; additional diagnostics allow

    The independent rejection expectation requires the match's missing bool coverage. A different parsing or name-resolution failure does not prove this claim. Additional diagnostics may be present; exact prose and diagnostic count are not fixed.

    Source evidence

    Cover both bool values

    Link
    example.tpzCover both bool valuesOpen in Playground
    1. function flag(value: bool) -> int {
    2. match (value) {
    3. case true => 1
    4. case false => 0
    5. }
    6. }
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz --compiler self · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0

    The repaired program adds the missing false case. Its expected result is successful checking of the complete fixture.

    Source evidence

    Reject different names across alternatives

    Link
    example.tpzReject different names across alternativesOpen in Playground
    1. function selected(value: Result<int, int>) -> int {
    2. match (value) {
    3. case Ok(left) | Err(right) => 0
    4. }
    5. }
    Recorded outputexit 1
    No stdout.
    error[TPZ5710]: or-pattern alternatives must bind the same names — `left` is bound in one alternative but not another
     --> example.tpz:3:14
      |
    3 |         case Ok(left) | Err(right) => 0
      |              ^^^^^^^^
    
    error[TPZ5710]: or-pattern alternatives must bind the same names — `right` is bound in one alternative but not another
     --> example.tpz:3:25
      |
    3 |         case Ok(left) | Err(right) => 0
      |                         ^^^^^^^^^^
    
    example.tpz: 2 type diagnostics
    
    $ topaz check example.tpz --compiler self · recorded with topaz 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5710 or-pattern-alternatives-bind-different-name-sets at example.tpz:3:14
    Observed assertions · passed · 5.21.3

    Exit code 1.

    No stdout.

    Observed stderr
    1. error[TPZ5710]: or-pattern alternatives must bind the same names — `left` is bound in one alternative but not another
    2. --> example.tpz:3:14
    3. |
    4. 3 | case Ok(left) | Err(right) => 0
    5. | ^^^^^^^^
    6. error[TPZ5710]: or-pattern alternatives must bind the same names — `right` is bound in one alternative but not another
    7. --> example.tpz:3:25
    8. |
    9. 3 | case Ok(left) | Err(right) => 0
    10. | ^^^^^^^^^^
    11. example.tpz: 2 type diagnostics
    • Passed · expected nonzero; observed 1
    • Passed · checking TPZ5710; cause or-pattern-alternatives-bind-different-name-sets; primary location asserted; additional diagnostics allow

    The intended cause is the unequal binding-name sets, not constructor arity or a missing case. The checker can report both directions of the disagreement. The accompanying accepted selected function binds item in both alternatives.

    Source evidence

    An unguarded catch-all makes the following case unreachable

    Link
    example.tpzAn unguarded catch-all makes the following case unreachableOpen in Playground
    1. let value: int = 7
    2. let selected = match (value) {
    3. case _ => "first"
    4. case later: int => "second"
    5. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5713 match-unreachable-case at example.tpz:4:10

    The first wildcard covers every integer. The later typed binding can never be selected and is rejected during checking. The rejection expects TPZ5713 during checking with cause match-unreachable-case.

    Source evidence

    A later wildcard is covered by two earlier unguarded cases

    Link
    example.tpzA later wildcard is covered by two earlier unguarded casesOpen in Playground
    1. let value: bool = true
    2. let result = match (value) {
    3. case true => "yes"
    4. case false => "no"
    5. case _ => "hidden"
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5713 match-unreachable-case at example.tpz:5:10

    The true and false cases together cover every Boolean value. Their union makes the final wildcard unreachable. The rejection expects TPZ5713 during checking with cause match-unreachable-case.

    Source evidence

    Every case is checked even after a catch-all

    Link
    example.tpzEvery case is checked even after a catch-allOpen in Playground
    1. let value: int = 7
    2. let selected = match (value) {
    3. case _ => "first"
    4. case impossible: string => "second"
    5. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5001 pattern-cannot-match-scrutinee-type at example.tpz:4:10

    The expected result follows from current SELF source. Execution against public Topaz 5.21.3 is pending CI.

    Source evidence

    One rest may precede, separate, or follow fixed elements

    Link
    example.tpzOne rest may precede, separate, or follow fixed elementsOpen in Playground
    1. import std.io
    2. let middle = match ([1, 2, 3, 4]) {
    3. case [first, ..rest, last] => "{first} {rest.length} {last}"
    4. case _ => "miss"
    5. }
    6. let leading = match ([1, 2, 3, 4]) {
    7. case [..rest, last] => "{rest.length} {last}"
    8. case _ => "miss"
    9. }
    10. let trailing = match ([1, 2, 3, 4]) {
    11. case [first, ..rest] => "{first} {rest.length}"
    12. case _ => "miss"
    13. }
    14. let empty: Array<int> = []
    15. let remaining = match (empty) {
    16. case [..rest] if (rest.length == 0) => rest.length
    17. case remaining => remaining.length
    18. }
    19. io.writeLine(middle)
    20. io.writeLine(leading)
    21. io.writeLine(trailing)
    22. io.writeLine("{remaining}")
    Expected outputexit 0
    1 2 4
    3 4
    1 3
    0
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 1 2 4
    2. 3 4
    3. 1 3
    4. 0

    The expected result follows from current SELF source. Execution against public Topaz 5.21.3 is pending CI.

    Source evidence

    Multiple rest elements retain an intended emission refusal

    Link
    example.tpzMultiple rest elements retain an intended emission refusalOpen in Playground
    1. let value = match ([1, 2]) {
    2. case [..left, ..right] => 0
    3. case _ => 1
    4. }
    Expected outputNonzero exit
    $ topaz emit example.tpz --compiler self --target rust · target product 5.21.3
    Expected result

    Expected result

    Rejection during emit. Nonzero exit.

    • TPZ6001 multiple-rest-emission-refusal

    The required primary-location contract is Pending.

    The existing Rust backend refuses this shape before generating output. Current SELF emission does not establish the same refusal. The rejection expectation is retained; its exact product diagnostic and location await verification.

    Source evidence

    Rejecting a structural field shorthand

    Link
    example.tpzRejecting a structural field shorthandOpen in Playground
    1. let { value } = { value: 1 }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self --format json
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 record-pattern-field-colon at example.tpz:1:13

    Write the field name, a colon, and its binding pattern. The missing colon is reported at the closing brace.

    Source evidence

    Rejecting a nominal field shorthand

    Link
    example.tpzRejecting a nominal field shorthandOpen in Playground
    1. record Point { x: int }
    2. let Point { x } = Point { x: 1 }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self --format json
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 record-pattern-field-colon at example.tpz:2:15

    Write the field name, a colon, and its binding pattern. The missing colon is reported at the closing brace.

    Source evidence

    Binding and renaming fields explicitly

    Link
    example.tpzBinding and renaming fields explicitlyOpen in Playground
    1. import std.io
    2. record Point { x: int, y: int }
    3. let { value: value } = { value: 7 }
    4. let Point { x: first, y: second } = Point { x: 2, y: 3 }
    5. io.writeLine("{value} {first} {second}")
    Expected outputexit 0
    7 2 3
    
    $ topaz run example.tpz --compiler self
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 7 2 3

    The same field mapping syntax binds a field to its own name or to a different name.

    Source evidence

    Rejecting a return without an arm block

    Link
    example.tpzRejecting a return without an arm blockOpen in Playground
    1. function choose(flag: bool) -> int {
    2. while (true) {
    3. let value = match (flag) {
    4. case true => return 7
    5. case false => 3
    6. }
    7. return value
    8. }
    9. 0
    10. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 match-arm-return-needs-block at example.tpz:4:26

    A return statement needs a block when used as a match arm body. The rejection points to the return keyword.

    Source evidence

    Returning through a match arm block

    Link
    example.tpzReturning through a match arm blockOpen in Playground
    1. import std.io
    2. function choose(flag: bool) -> int {
    3. while (true) {
    4. let value = match (flag) {
    5. case true => { return 7 }
    6. case false => 3
    7. }
    8. return value
    9. }
    10. 0
    11. }
    12. io.writeLine("{choose(true)}")
    13. io.writeLine("{choose(false)}")
    Expected outputexit 0
    7
    3
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 7
    2. 3

    The true arm returns from the function through the surrounding binding and loop. The false arm supplies a value to the ordinary return statement.

    Source evidence

    Rejecting a rest marker without a pattern

    Link
    example.tpzRejecting a rest marker without a patternOpen in Playground
    1. let count = match ([1, 2]) {
    2. case [..] => 2
    3. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 list-rest-pattern-required at example.tpz:2:13

    Write .._ to ignore the rest. The error points to the delimiter where the required pattern is missing.

    Source evidence

    Rejecting an omitted middle rest pattern

    Link
    example.tpzRejecting an omitted middle rest patternOpen in Playground
    1. let total = match ([1, 2, 3]) {
    2. case [first, .., last] => first + last
    3. case _ => 0
    4. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 list-rest-pattern-required at example.tpz:2:20

    Write .._ to ignore the rest. The error points to the delimiter where the required pattern is missing.

    Source evidence

    Ignoring a rest with an explicit wildcard

    Link
    example.tpzIgnoring a rest with an explicit wildcardOpen in Playground
    1. import std.io
    2. let middle = match ([1, 2, 3, 4]) {
    3. case [first, .._, last] => "{first} {last}"
    4. case _ => "miss"
    5. }
    6. let leading = match ([1, 2, 3, 4]) {
    7. case [.._, last] => last
    8. case _ => 0
    9. }
    10. let trailing = match ([1, 2, 3, 4]) {
    11. case [first, .._] => first
    12. case _ => 0
    13. }
    14. let empty: Array<int> = []
    15. let ignored = match (empty) {
    16. case [.._] if (empty.length == 0) => true
    17. case _ => false
    18. }
    19. io.writeLine(middle)
    20. io.writeLine("{leading}")
    21. io.writeLine("{trailing}")
    22. io.writeLine("{ignored}")
    Expected outputexit 0
    1 4
    4
    1
    true
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 1 4
    2. 4
    3. 1
    4. true

    The wildcard ignores a leading, middle, or trailing slice. A rest wildcard also matches an empty array.

    Source evidence

    Constructor patterns keep parentheses at every arity

    Link
    example.tpzConstructor patterns keep parentheses at every arityOpen in Playground
    1. import std.io
    2. enum Choice { Empty, Filled(int), Pair(int, int) }
    3. function read(value: Choice) -> int {
    4. match (value) {
    5. case Empty() => 0
    6. case Filled(number) => number
    7. case Pair(left, right) => left + right
    8. }
    9. }
    10. function nested(value: Option<Choice>) -> int {
    11. match (value) {
    12. case None() => -1
    13. case Some(Empty()) => 0
    14. case Some(Filled(number)) => number
    15. case Some(Pair(left, right)) => left + right
    16. }
    17. }
    18. io.writeLine("{read(Choice.Empty())}")
    19. io.writeLine("{read(Choice.Filled(7))}")
    20. io.writeLine("{read(Choice.Pair(2, 3))}")
    21. io.writeLine("{nested(None())}")
    22. io.writeLine("{nested(Some(Choice.Empty()))}")
    Expected outputexit 0
    0
    7
    5
    -1
    0
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 0
    2. 7
    3. 5
    4. -1
    5. 0

    Value expressions and constructor patterns keep parentheses at every arity. The nested match covers every Option and Choice variant.

    Source evidence

    A fresh bare name captures an enum value

    Link
    example.tpzA fresh bare name captures an enum valueOpen in Playground
    1. import std.io
    2. enum Choice { Empty, Filled(int) }
    3. function identity(value: Choice) -> Choice {
    4. match (value) {
    5. case captured => captured
    6. }
    7. }
    8. function read(value: Choice) -> int {
    9. match (value) {
    10. case Empty() => 0
    11. case Filled(number) => number
    12. }
    13. }
    14. io.writeLine("{read(identity(Choice.Filled(8)))}")
    Expected outputexit 0
    8
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 8

    The bare name captured is a binding in every expected type. SELF currently treats an unknown top-level enum-case spelling as a variant typo, so this intended acceptance is recorded under the implementation limitation.

    Source evidence

    A fresh typed name captures the whole enum

    Link
    example.tpzA fresh typed name captures the whole enumOpen in Playground
    1. import std.io
    2. enum Choice { Empty, Filled(int) }
    3. function identity(value: Choice) -> Choice {
    4. match (value) {
    5. case captured: Choice => captured
    6. }
    7. }
    8. function upper(value: int) -> int {
    9. match (value) {
    10. case UPPER => UPPER
    11. }
    12. }
    13. function read(value: Choice) -> int {
    14. match (value) {
    15. case Empty() => 0
    16. case Filled(number) => number
    17. }
    18. }
    19. io.writeLine("{read(identity(Choice.Filled(8)))}")
    20. io.writeLine("{upper(9)}")
    Expected outputexit 0
    8
    9
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 8
    2. 9

    An annotation refines the matched type. It does not turn a binding into a constructor or permit a constructor-name collision. An uppercase fresh name is still a binding.

    Source evidence

    A bare nullary variant cannot become a catch-all

    Link
    example.tpzA bare nullary variant cannot become a catch-allOpen in Playground
    1. enum Choice { Empty, Filled(int) }
    2. function read(value: Choice) -> int {
    3. match (value) {
    4. case Empty => 0
    5. case Filled(number) => number
    6. }
    7. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5712 pattern-constructor-binding-collision at example.tpz:4:14

    Write Empty() to test the nullary variant. The removed spelling must be rejected instead of accepting every Choice value.

    Source evidence

    Nested constructors require the same argument list

    Link
    example.tpzNested constructors require the same argument listOpen in Playground
    1. enum Choice { Empty, Filled(int) }
    2. function read(value: Option<Choice>) -> int {
    3. match (value) {
    4. case Some(Empty) => 0
    5. case _ => 1
    6. }
    7. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5712 pattern-constructor-binding-collision at example.tpz:4:19

    Write Some(Empty()). Nesting does not turn a constructor spelling into a binding.

    Source evidence

    A type annotation cannot bypass a constructor-name collision

    Link
    example.tpzA type annotation cannot bypass a constructor-name collisionOpen in Playground
    1. enum Choice { Empty, Filled(int) }
    2. function read(value: Choice) -> int {
    3. match (value) {
    4. case Empty: Choice => 0
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5712 pattern-constructor-binding-collision at example.tpz:4:14

    Choose a fresh name such as captured: Choice for a typed catch-all, or write Empty() to test the variant.

    Source evidence

    Constructor names stay reserved for patterns across expected types

    Link
    example.tpzConstructor names stay reserved for patterns across expected typesOpen in Playground
    1. enum Mode { Idle }
    2. function read(value: int) -> int {
    3. match (value) {
    4. case Idle => 0
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5712 pattern-constructor-binding-collision at example.tpz:4:14

    Idle is visible in the constructor namespace. Changing the scrutinee type or introducing a value with that name does not make this spelling a pattern binding.

    Source evidence

    None needs parentheses in a constructor pattern

    Link
    example.tpzNone needs parentheses in a constructor patternOpen in Playground
    1. function read(value: Option<int>) -> int {
    2. match (value) {
    3. case Some(number) => number
    4. case None => 0
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2012 pattern-reserved-none-binding at example.tpz:4:14

    Write None() for the constructor pattern. None remains a reserved binding name. The rejection uses the existing TPZ2012 identity and reserved-None producer message.

    Source evidence

    None needs parentheses in a constructor value

    Link
    example.tpzNone needs parentheses in a constructor valueOpen in Playground
    1. let maybe: Option<int> = None
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2012 pattern-reserved-none-binding at example.tpz:1:26

    Write None() when constructing an absent Option value. Bare None is rejected during parsing with TPZ2012 and the same reserved-None cause as the bare pattern example.

    Source evidence

    Specific cases before a general fallback

    Link
    example.tpzSpecific cases before a general fallbackOpen in Playground
    1. import std.io
    2. function first(value: int) -> string {
    3. match (value) {
    4. case 1 => "one"
    5. case _ => "rest"
    6. }
    7. }
    8. function typed(value: int) -> string {
    9. match (value) {
    10. case 1 => "one"
    11. case captured: int => "rest"
    12. }
    13. }
    14. function last(value: int) -> string {
    15. match (value) {
    16. case 2 => "two"
    17. case 1 => "one"
    18. case _ => "rest"
    19. }
    20. }
    21. io.writeLine("{first(1)} {typed(1)} {last(1)}")
    22. io.writeLine("{first(3)} {typed(3)} {last(3)}")
    Expected outputexit 0
    one one one
    rest rest rest
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. one one one
    2. rest rest rest

    Specific cases are tried first. The wildcard and total typed binding handle values that reach them. Putting the general case first makes the following specific cases unreachable and causes a checking error.

    Source evidence

    A failed guard lets the same pattern match later

    Link
    example.tpzA failed guard lets the same pattern match laterOpen in Playground
    1. import std.io
    2. function check(label: string, ok: bool) -> bool {
    3. io.writeLine(label)
    4. ok
    5. }
    6. function choose(value: int) -> string {
    7. match (value) {
    8. case 2 if (check("guard for two", false)) => "guarded two"
    9. case 2 => "unguarded two"
    10. case _ => "other"
    11. }
    12. }
    13. io.writeLine(choose(2))
    14. io.writeLine(choose(3))
    Expected outputexit 0
    guard for two
    unguarded two
    other
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. guard for two
    2. unguarded two
    3. other

    The first case matches 2 but its guard is false. Selection continues to the next case, which uses the same pattern without a guard. The other input reaches the wildcard.

    Source evidence

    Cover type and list domains with a record fallback

    Link
    example.tpzCover type and list domains with a record fallbackOpen in Playground
    1. import std.io
    2. function kind(value: int | string) -> string {
    3. match (value) {
    4. case number: int => "integer"
    5. case text: string => "string"
    6. }
    7. }
    8. function row(value: { x: int, y: int }) -> string {
    9. match (value) {
    10. case { x: 0, y: _ } => "zero"
    11. case { x: 1, y: _ } => "one"
    12. case _ => "other"
    13. }
    14. }
    15. function size(values: Array<int>) -> string {
    16. match (values) {
    17. case [] => "empty"
    18. case remaining => "nonempty"
    19. }
    20. }
    21. io.writeLine(kind(1))
    22. io.writeLine(kind("a"))
    23. io.writeLine(row({ x: 0, y: 9 }))
    24. io.writeLine(row({ x: 1, y: 9 }))
    25. io.writeLine(size([]))
    26. io.writeLine(size([1]))
    Expected outputexit 0
    integer
    string
    zero
    one
    empty
    nonempty
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. integer
    2. string
    3. zero
    4. one
    5. empty
    6. nonempty

    The typed cases cover the union. The list cases cover empty and nonempty arrays. The record match reaches its final case for other field values.

    Source evidence

    Adjacent half-open and closed intervals do not overlap

    Link
    example.tpzAdjacent half-open and closed intervals do not overlapOpen in Playground
    1. import std.io
    2. function band(value: int) -> string {
    3. match (value) {
    4. case (0 ..< 5) => "low"
    5. case (5 .. 9) => "high"
    6. case _ => "outside"
    7. }
    8. }
    9. io.writeLine(band(0))
    10. io.writeLine(band(4))
    11. io.writeLine(band(5))
    12. io.writeLine(band(9))
    13. io.writeLine(band(10))
    Expected outputexit 0
    low
    low
    high
    high
    outside
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. low
    2. low
    3. high
    4. high
    5. outside

    The first interval excludes 5 and the second includes it. The wildcard follows both intervals and handles values outside them.

    Source evidence

    Dynamic bounds with a later catch-all

    Link
    example.tpzDynamic bounds with a later catch-allOpen in Playground
    1. import std.io
    2. function bound(value: int) -> int { value }
    3. function band(value: int, low: int, high: int) -> string {
    4. match (value) {
    5. case (low ..< bound(high)) => "inside"
    6. case _ => "outside"
    7. }
    8. }
    9. io.writeLine(band(2, 0, 5))
    10. io.writeLine(band(5, 0, 5))
    Expected outputexit 0
    inside
    outside
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. inside
    2. outside

    The dynamic range is tried before the wildcard. Its runtime bound test selects the first case when it succeeds. Otherwise the wildcard handles the value.

    Source evidence

    A repeated unguarded literal case is unreachable

    Link
    example.tpzA repeated unguarded literal case is unreachableOpen in Playground
    1. function choose(value: int) -> int {
    2. match (value) {
    3. case 1 => 10
    4. case 1 => 20
    5. case _ => 0
    6. }
    7. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5713 match-unreachable-case at example.tpz:4:14

    The first unguarded case already handles every value the second identical literal pattern can match. The rejection expects TPZ5713 during checking with cause match-unreachable-case.

    Source evidence

    A specific constructor can precede a general constructor

    Link
    example.tpzA specific constructor can precede a general constructorOpen in Playground
    1. import std.io
    2. function choose(value: Option<int>) -> string {
    3. match (value) {
    4. case Some(0) => "zero"
    5. case Some(number) => "other {number}"
    6. case None() => "none"
    7. }
    8. }
    9. io.writeLine(choose(Some(0)))
    10. io.writeLine(choose(Some(7)))
    11. io.writeLine(choose(None()))
    Expected outputexit 0
    zero
    other 7
    none
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. zero
    2. other 7
    3. none

    Some(0) matches both constructor patterns. The first handles zero while the next handles other payloads.

    Source evidence

    Overlapping record patterns leave later inputs

    Link
    example.tpzOverlapping record patterns leave later inputsOpen in Playground
    1. import std.io
    2. function choose(value: { x: int, y: int }) -> string {
    3. match (value) {
    4. case { x: 0 } => "x"
    5. case { y: 0 } => "y"
    6. case _ => "other"
    7. }
    8. }
    9. io.writeLine(choose({ x: 0, y: 0 }))
    10. io.writeLine(choose({ x: 3, y: 0 }))
    11. io.writeLine(choose({ x: 3, y: 4 }))
    Expected outputexit 0
    x
    y
    other
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. x
    2. y
    3. other

    Both field tests succeed for the first value, so the first case wins. A value with nonzero x and zero y still reaches the second case.

    Source evidence

    Overlapping list patterns follow source order

    Link
    example.tpzOverlapping list patterns follow source orderOpen in Playground
    1. import std.io
    2. function choose(values: Array<int>) -> string {
    3. match (values) {
    4. case [1, .._] => "starts with one"
    5. case [_, 1] => "ends with one"
    6. case _ => "other"
    7. }
    8. }
    9. io.writeLine(choose([1, 1]))
    10. io.writeLine(choose([2, 1]))
    11. io.writeLine(choose([2, 2]))
    Expected outputexit 0
    starts with one
    ends with one
    other
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. starts with one
    2. ends with one
    3. other

    The first pattern handles arrays starting with one. The second can still handle arrays ending with one that do not start with one.

    Source evidence

    Overlapping guarded cases follow source order

    Link
    example.tpzOverlapping guarded cases follow source orderOpen in Playground
    1. import std.io
    2. function choose(value: int) -> string {
    3. match (value) {
    4. case number if (number > 0) => "positive"
    5. case number if (number <= 0) => "nonpositive"
    6. case _ => "other"
    7. }
    8. }
    9. io.writeLine(choose(4))
    10. io.writeLine(choose(0))
    Expected outputexit 0
    positive
    nonpositive
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. positive
    2. nonpositive

    Both binding patterns accept every integer. The first guard decides whether to continue to the next case, and neither guard establishes unconditional coverage.

    Source evidence

    Overlapping alternatives describe one match case

    Link
    example.tpzOverlapping alternatives describe one match caseOpen in Playground
    1. import std.io
    2. function choose(value: int) -> string {
    3. match (value) {
    4. case 1 | 1 => "one"
    5. case _ => "other"
    6. }
    7. }
    8. io.writeLine(choose(1))
    9. io.writeLine(choose(3))
    Expected outputexit 0
    one
    other
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. one
    2. other

    Two alternatives can recognize the same input without creating different case priorities. Both alternatives select the same body.

    Source evidence

    Overlapping integer intervals keep later cases reachable

    Link
    example.tpzOverlapping integer intervals keep later cases reachableOpen in Playground
    1. import std.io
    2. function choose(value: int) -> string {
    3. match (value) {
    4. case (0 .. 5) => "lower"
    5. case (5 ..< 9) => "upper"
    6. case _ => "outside"
    7. }
    8. }
    9. io.writeLine(choose(5))
    10. io.writeLine(choose(7))
    11. io.writeLine(choose(10))
    Expected outputexit 0
    lower
    upper
    outside
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. lower
    2. upper
    3. outside

    Five belongs to both intervals and selects the first. Seven reaches the second interval. The intervals do not need a disjointness proof.

    Source evidence

    Dynamic range overlap uses ordinary source order

    Link
    example.tpzDynamic range overlap uses ordinary source orderOpen in Playground
    1. import std.io
    2. function bound(value: int) -> int { value }
    3. function choose(value: int, low: int, high: int) -> string {
    4. match (value) {
    5. case (low ..< bound(high)) => "range"
    6. case 100 => "hundred"
    7. case _ => "outside"
    8. }
    9. }
    10. io.writeLine(choose(100, 0, 5))
    11. io.writeLine(choose(100, 0, 105))
    12. io.writeLine(choose(200, 0, 105))
    Expected outputexit 0
    hundred
    range
    outside
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. hundred
    2. range
    3. outside

    When the dynamic range excludes 100, the literal case succeeds. When it includes 100, the earlier range succeeds. An unknown intersection does not require rejection.

    Source evidence

    Reject an unwrapped range pattern

    Link
    example.tpzReject an unwrapped range patternOpen in Playground
    1. let value = match (2) {
    2. case 1 .. 3 => 1
    3. case _ => 0
    4. }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz --compiler self --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during parse. Nonzero exit.

    • TPZ2001 range-pattern-parentheses at example.tpz:2:10
    Source evidence

    A delimited range precedes a lambda-valued arm

    Link
    example.tpzA delimited range precedes a lambda-valued armOpen in Playground
    1. import std.io
    2. let low = 1
    3. let high = 3
    4. let chosen = match (2) {
    5. case (low .. high) => (value: int) => value + 1
    6. case _ => (value: int) => value
    7. }
    8. io.writeLine("{chosen(6)}")
    Expected outputexit 0
    7
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 7
    Source evidence

    A missing enum variant is rejected

    Link
    example.tpzA missing enum variant is rejectedOpen in Playground
    1. enum Choice { Left(int), Right(int) }
    2. function read(value: Choice) -> int {
    3. match (value) {
    4. case Left(number) => number
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5021 match-not-exhaustive at example.tpz:3:5
    Source evidence

    Both enum variants are covered

    Link
    example.tpzBoth enum variants are coveredOpen in Playground
    1. import std.io
    2. enum Choice { Left(int), Right(int) }
    3. function read(value: Choice) -> int {
    4. match (value) {
    5. case Left(number) => number
    6. case Right(number) => number + 10
    7. }
    8. }
    9. io.writeLine("{read(Choice.Left(2))}")
    10. io.writeLine("{read(Choice.Right(3))}")
    Expected outputexit 0
    2
    13
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 2
    2. 13
    Source evidence

    An integer match needs a final fallback

    Link
    example.tpzAn integer match needs a final fallbackOpen in Playground
    1. function read(value: int) -> int {
    2. match (value) {
    3. case 0 => 10
    4. }
    5. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5021 match-not-exhaustive at example.tpz:2:5
    Source evidence

    A final name pattern covers remaining integers

    Link
    example.tpzA final name pattern covers remaining integersOpen in Playground
    1. import std.io
    2. function read(value: int) -> int {
    3. match (value) {
    4. case 0 => 10
    5. case remaining => remaining
    6. }
    7. }
    8. io.writeLine("{read(0)}")
    9. io.writeLine("{read(7)}")
    Expected outputexit 0
    10
    7
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 10
    2. 7
    Source evidence

    Guarded bool arms do not establish coverage

    Link
    example.tpzGuarded bool arms do not establish coverageOpen in Playground
    1. function read(value: bool) -> int {
    2. match (value) {
    3. case true if (true) => 1
    4. case false if (true) => 0
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5021 match-not-exhaustive at example.tpz:2:5
    Source evidence

    A missing nested bool payload is rejected

    Link
    example.tpzA missing nested bool payload is rejectedOpen in Playground
    1. function read(value: Option<bool>) -> int {
    2. match (value) {
    3. case Some(true) => 1
    4. case None() => -1
    5. }
    6. }
    Expected outputNonzero exit
    $ topaz check example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5021 match-not-exhaustive at example.tpz:2:5
    Source evidence

    Option bool covers both payloads and None

    Link
    example.tpzOption bool covers both payloads and NoneOpen in Playground
    1. import std.io
    2. function read(value: Option<bool>) -> int {
    3. match (value) {
    4. case Some(true) => 1
    5. case Some(false) => 0
    6. case None() => -1
    7. }
    8. }
    9. let flags = map { "on": true, "off": false }
    10. io.writeLine("{read(flags.get("on"))}")
    11. io.writeLine("{read(flags.get("off"))}")
    12. io.writeLine("{read(flags.get("missing"))}")
    Expected outputexit 0
    1
    0
    -1
    
    $ topaz run example.tpz --compiler self · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 1
    2. 0
    3. -1

    Map lookup supplies Some and None values. Exhaustiveness includes each closed bool payload inside Some.

    Source evidence

    Pending contracts

    Link

    Related diagnostics and owners

    Link

    Non-exhaustive match

    Or-pattern alternatives bind different names

    See Blocks and control flow for if let, while let, and return; Expressions and operators for expression grouping and range expressions; and Functions and generics for function declarations and return inference. A pattern range is defined on this page; the expression form has its own owner.

    Unexpected token

    TPZ5001

    Native emitter cannot lower this construct yet

    Constructor name used as a pattern binding

    Reserved binding name

    Unreachable match case

    Or-pattern binding types disagree

    Menu

    Download