Table of Contents

Fundamental patterns

Fundamental patterns check a value's type or contents. A successful pattern can also give the matched value a name for use in that branch.

Type and binding patterns

  • Typetype pattern. Succeeds when the scrutinee can be treated as Type. If the pattern introduces no designation, it behaves like a type test.

  • Type nametyped binding. Succeeds when the scrutinee can be treated as Type, then binds the converted value to name as an immutable local in the success scope.

    When Type is an open generic type name written without explicit type arguments (for example Box where Box<T> exists), Raven infers type arguments from the scrutinee when possible.

    This inference applies uniformly in:

    • is patterns (if value is Box box { ... })
    • match expression arms (match value { Box box => ... })
    • match statement arms (match value { Box box => ... })

    Example:

    class Box<T> {}
    let value: Box<int> = Box<int>()
    
    if value is Box box {
        // box : Box<int>
    }
    
  • let name / var name / val namevariable pattern. Always matches and introduces a binding. let/val produce an immutable local; var produces a mutable one.

    Parenthesized designations such as let (first, second): (int, string) bind each element positionally.

  • Explicit binding keyword required. In pattern position, introducing a new binding always requires an explicit binding keyword (let, var, or val). A bare identifier never introduces a binding. It may name a type or a compile-time constant, but runtime values require an explicit == value comparison pattern.

    For example:

    • Ok(42) matches the literal value 42.
    • Ok(== discountedProduct) matches the runtime value of the in-scope symbol discountedProduct.
    • Ok(let n) binds the payload to a new immutable local n.

    The same rule applies uniformly in positional patterns and discriminated-union case payloads.

Discards

  • _ / Type _discard. Matches without introducing a binding. The typed form asserts the value can be treated as Type while still discarding it. Because _ is reserved for discards, writing _ never creates a binding.

    Discards participate in exhaustiveness: an unguarded _ arm is a catch-all and satisfies any remaining cases (even if earlier arms introduced bindings).

Constant patterns

  • literalconstant pattern. Matches when the scrutinee equals the literal value (true, "on", 42, or null).

  • identifiertype or compile-time constant pattern. A bare identifier may name a type or a compile-time constant. A local, parameter, property, or non-constant field is rejected here; write == identifier to compare with its runtime value, or use let/var/val to introduce a binding.

  • Type.Memberqualified constant/value pattern. When the qualified name resolves to a static constant-like value, such as Math.PI or an enum member such as JsonValueKind.True, the pattern matches when the scrutinee equals that value. Qualified names that resolve to types remain type/declaration patterns.

  • .Membertarget-typed value pattern. When the scrutinee type is known (for example, an enum type or a type with static fields), the leading-dot expression resolves against that target type and matches the resulting value. Equality expressions also provide a target type from the opposite operand, so value == .Member is equivalent to value == EnumType.Member when value has the enum/member-bearing type.

A bare identifier in pattern position is context-sensitive only between a type and a compile-time constant. Runtime value comparisons are always explicit.

Comparison patterns

  • < expr, <= expr, > expr, >= expr, == expr, != exprcomparison pattern. Matches when the scrutinee compares to the operand using the given operator.

    The operand must be a side-effect-free expression (for example, literals, constants, or other stable values), ensuring comparison patterns remain predictable and optimizable.

    Explicit runtime-value comparisons and guarded bindings work uniformly in nested patterns. For example, all of these case payloads use the general pattern rules: Ok(== discountedProduct), Ok(let productName when == discountedProduct), Ok(let productName when productName == discountedProduct), and Ok(let productName: string when productName == discountedProduct).

    The operand type must match the scrutinee type after nullable/plain-type unwrapping. Ordinary implicit numeric widening is not applied inside comparison patterns, so matching an int scrutinee with > 0.5 is an error.

    Comparison patterns are commonly used under not, and, and property patterns, e.g. { Age: not > 30 }.

Range patterns

A range pattern matches values that fall within a lower and/or upper bound using .. (inclusive upper bound) or ..< (exclusive upper bound). Range patterns are valid when the scrutinee type is orderable (for example numeric types, char, or other types that support relational comparison).

Like comparison patterns, range bounds must match the scrutinee type after nullable/plain-type unwrapping; normal implicit numeric conversions are not applied to range bounds.

Both bounds are optional:

  • lo..hi — matches values greater than or equal to lo and less than or equal to hi.
  • lo..<hi — matches values greater than or equal to lo and less than hi.
  • ..hi — matches values less than or equal to hi.
  • ..<hi — matches values less than hi.
  • lo.. — matches values greater than or equal to lo.

Bounds are written as expressions, but Raven may require constant-like values in contexts where the range must be known during compilation.

let value = 42

let result = match value {
    ..4 => "No"
    40..43 => "Yes"
    _ => "Other"
}

Range patterns participate in exhaustiveness and subsumption analysis alongside comparison patterns. They are treated as syntactic sugar for a conjunction of comparison operators (for example 40..43 behaves like >= 40 and <= 43).

🧭 Disambiguation: In pattern position, .. introduces a range pattern rather than a range expression. The expression parser stops at .. so the bounds are parsed independently.

Positional patterns

  • (element1, element2, …)positional pattern. Matches when the scrutinee can be deconstructed positionally with the same arity (either as a tuple or via a compatible Deconstruct method).

    Positional patterns destructure by position. Each element is itself a pattern and may introduce bindings. For example:

    • If the scrutinee type exposes a Deconstruct method with matching arity (including as an extension method), the positional pattern uses that method to obtain positional values.

    • (let a, var b) (explicit mutability)

    • (a: int, b: string) (inline type annotations)

    • (int a, string b) (type-pattern + capture)

    • (a: int, _)

    • (let a, == existingValue) (explicit capture + value pattern)

    • (== existingA, == existingValue) (existing-value comparisons)

    In freestanding and inline positional patterns, captured variables must use an explicit binding keyword (let, var, or val). Comparing against an existing runtime value uses == existingValue. In assignment and declaration deconstruction (let (a, b) = expr, (a, b) = expr), bare identifiers continue to act as deconstruction targets. To constrain by type and capture a value in an element, both forms are valid: let name: Type (Raven-native typed binding style) and Type name (type-pattern style). This is equivalent to introducing a binding and adding a when guard that compares the bound value, but == expr keeps the constraint local to the pattern and avoids an additional arm condition.

    An element may optionally include a name before the colon (name: pattern) to bind the element value while still applying a nested pattern. When the positional pattern is backed by Deconstruct rather than a tuple, named elements bind by Deconstruct parameter name and may appear in any order. Unnamed elements continue to consume the remaining unmatched parameters in declaration order. If a supplied name does not match any supported deconstruction member/parameter, the pattern is invalid.