Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Patterns and Pattern-Matching

A match expression dispatches on a value’s shape. In Argon you reach for it in two settings: when destructuring data (a lease that might be residential, commercial, or general), and when handling reasoning outcomes (a query that might be unknown, ambiguous, or have timed out).

This chapter teaches both.

A first match

pub compute lease_summary(l: Lease) -> String =
    match l {
        ResidentialLease(r) => "Residential, " + r.bedrooms + " bedrooms",
        CommercialLease(c) => "Commercial: " + c.permitted_use,
        _ => "General lease",
    }

Three things to notice.

The scrutinee is the expression being matched. Here, l. Match arms are tried in source order; the first matching pattern fires.

Each arm is pattern => body. The body is any expression — a string, a method call, another match. The arrow => is the same => used elsewhere in the language; the parser disambiguates by context.

Trailing comma is allowed. Use it; rebasing match arms is friendlier when the last arm has a comma too.

Six pattern forms

Pattern formExampleUse
Wildcard_Match anything; bind nothing
Literal42, "active", #2026-01-01#Match a specific value
VariantResidentialLease(r)Match a subtype, bind the value
Named-with-guardr: ResidentialLease where r.bedrooms > 4Match + filter
Type testv: TenantMatch by type, bind the value
Reasoning outcomeis unknown, is ambiguous(x), is timeout(x)Match a reasoner’s answer

The first three are the bread-and-butter. The last three are situational.

Wildcard

match status {
    "active" => 1,
    "pending" => 0,
    _ => -1,
}

_ matches anything that did not match earlier arms. It is the conventional default.

Literal

match phase_name {
    "Pending" => initial_state(),
    "Active" => active_state(),
    _ => unknown_state(),
}

Literal patterns match the exact value. Int, Decimal, String, Bool, and Date literals are all admitted.

Variant

When the scrutinee’s static type is a parent concept and you want to dispatch on which subtype it actually is:

pub compute permitted_use_or_bedrooms(l: Lease) -> String =
    match l {
        ResidentialLease(r) => r.bedrooms.to_string(),
        CommercialLease(c) => c.permitted_use,
        _ => "unspecified",
    }

ResidentialLease(r) reads “if l is a ResidentialLease, bind it to r and use that binding in the body.” The variable r is typed as ResidentialLease inside the arm body.

Type test

match v {
    p: Person => p.name,
    o: Organization => o.legal_name,
    _ => "unknown",
}

p: Person is shorthand for the variant pattern when you want the binding’s type to drive the matching. The semantics are the same; the spelling is the one most programmers reach for first.

Named-with-guard

A pattern can carry an inline filter:

match lease {
    l: ResidentialLease where l.bedrooms > 4 => "large residential",
    l: ResidentialLease => "residential",
    l: CommercialLease => "commercial",
    _ => "other",
}

The where clause is a single rule-atom — comparisons, type tests, predicate calls. Use guards when the same type maps to multiple semantic categories.

Reasoning outcomes

Argon’s reasoner returns three-valued results. A query under the open-world assumption can return:

  • A concrete result.
  • unknown — the reasoner cannot decide.
  • ambiguous(x) — multiple inconsistent derivations.
  • timeout(x) — the reasoner gave up before deciding.

A match over a query result handles each:

match ActiveLease(l) {
    is unknown => Pending,
    is ambiguous(_) => undecided_state(),
    is timeout(_) => undecided_state(),
    _ => Active,
}

This is the surface for handling reasoning-outcome cases inside compute bodies. We use it sparingly in the running tutorial; most rules sit at tier 0–3 where outcomes are decided.

Each of the six pattern forms ships with a runnable workspace under argon/examples/_catalog/match-pattern-{literal,variant,wildcard,guard}/ and _catalog/match-expression/. The match-expression directory in particular has variants for each match-in-context shape: against-literal/, against-variant/, with-wildcard/, with-guard/, composition-rule-body/. Each is a self-contained workspace — cd and ox check.

A worked example from _catalog/match-pattern-guard/with-cross-axis-guard/src/prelude.ar — a pattern arm with a multi-axis guard:

pub compute classify_payment_state(l: Lease) -> PaymentState =
    match l {
        ActiveLease(a) if a.is_current and a.balance == zero => Current,
        ActiveLease(a) if a.balance > zero => InArrears,
        ActiveLease(_) => Active,
        TerminatedLease(_) => Settled,
        _ => Unknown,
    }

The guard’s body uses the rule-body sublanguage from Chapter 2.4; it can reference fields, call computes, and probe the metatype calculus. The compiler enforces exhaustiveness after guard analysis, so an unreached arm produces OE0203 not silent fallthrough.

Exhaustiveness

The compiler checks that every match has a way of reaching a body. If a match has no wildcard and no guardless variant covering every possible subtype, the compiler reports OE0203 — non-exhaustive match:

match l {
    ResidentialLease(r) => r.bedrooms.to_string(),
    CommercialLease(c) => c.permitted_use,
    // OE0203 — what about a bare `Lease`?
}

Adding _ => "general" as the last arm fixes this. Adding l: Lease => "general" (a guardless type test for the parent type) also fixes it.

The exhaustiveness check is static — it runs at ox check. You do not pay for it at runtime.

match in different contexts

match is a single expression form, but it shows up in three places:

Compute bodies

The most common — a single-expression compute returns the result of a match:

pub compute lease_summary(l: Lease) -> String =
    match l { ... }

Mutation bodies

Inside a do { } block, a let can bind the result of a match:

do {
    let category = match l {
        r: ResidentialLease => "residential",
        c: CommercialLease => "commercial",
        _ => "general",
    }
    l.category_label = category
}

Rule bodies

A whole rule body can be a single match expression — useful when the body splits cleanly along subtype lines:

pub derive lease_category(l: Lease, cat: String) :-
    match l {
        r: ResidentialLease => cat = "residential",
        c: CommercialLease => cat = "commercial",
        _ => cat = "general",
    }

The match is desugared into a disjunction of derive rules with subtype guards.

Edge cases worth knowing

  • Match arms are tried in source order. A more general pattern earlier in the list will short-circuit later, more specific arms.
  • Bindings are scoped to the arm body. r in ResidentialLease(r) => is not visible after the arrow leaves.
  • Type tests narrow. Inside r: ResidentialLease =>, r.bedrooms is well-typed because the compiler knows r is a ResidentialLease. This is occurrence typing (occurrence narrowing); the compiler tracks per-arm refinements automatically.
  • is unknown and friends are different from _ where outcome == unknown. The is-family is a dedicated pattern shape because reasoning outcomes are not ordinary values; they live in a sum-type at the engine level that the regular comparison operators do not reach into.

Putting it in the running example

We do not add a dedicated patterns file — match shows up where it fits. Our running tutorial threads match into existing computes and mutations. For example, computes.ar could grow:

use lease::*

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

pub compute lease_summary(l: Lease) -> String =
    match l {
        r: ResidentialLease => "Residential, " + r.bedrooms + " bedrooms",
        c: CommercialLease => "Commercial, use: " + c.permitted_use,
        _ => "General lease",
    }
$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.13s

Summary

match dispatches on a value’s shape. Six pattern forms cover the everyday cases — wildcard, literal, variant, type test, named-with-guard, and reasoning outcome. The compiler enforces exhaustiveness at ox check; occurrence typing narrows bindings inside arms. match shows up in compute bodies, mutation bodies, and rule bodies; the surface is the same in all three.