Language reference

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

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

  • Type name — typed 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. Inference runs before missing-type-argument validation; a successfully inferred type does not produce a missing-arguments diagnostic. If the scrutinee does not supply a matching constructed type (for example, its static type is object or another generic definition), explicit arguments are still required.

    Explicit closed generic arguments remain part of the pattern type. For example, an arm for Box<int> does not make a later Box<long> arm unreachable merely because both use Box<T>. Repeating the same closed type without a guard still makes the later identical type arm unreachable.

    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>
    }
    

Function types can be used directly as type or typed-binding patterns:

func IsFactory<T>(obj: object) -> bool {
    if obj is () -> T {
        return true
    }
    return false
}

func Read<T>(obj: object, fallback: T) -> T {
    return obj match {
        () -> T callback => callback()
        _ => fallback
    }
}

The -> after a parameter list distinguishes a function type from a positional pattern. This also supports signatures with parameters, such as (int) -> int callback, and nested function types. Generic parameters refer to those already in scope; matching does not infer a new type parameter at runtime. Null does not match. On .NET, the test uses the standard delegate representation of the function type and the platform's delegate compatibility rules. The neoCLR target uses its native structural function signature and runtime compatibility. The shared syntax does not add function variance or dynamic invocation semantics.

  • let name / var name / val name — variable 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

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

  • identifier — type 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.Member — qualified 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.

  • .Member — target-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, != expr — comparison 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 with ..; ..= requires an upper bound:

  • 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: A range pattern tests bounds. Within a sequence pattern, ..N introduces a fixed segment and ...let rest matches the remaining elements; these are sequence prefixes, not range patterns. A slice such as items[1..3] is an element-access expression consuming a range, not a pattern.

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.