ADR-352: null Is a Pattern in a match Over a T?

Status: Accepted Date: October 14, 2026 Answers: how a match takes a T? apart (issue #612) Related: ADR-279 (nullable types); ADR-291 (patterns, else, guards); ADR-382 (a test that proves a T? is not null); Part I 3.4; Part III 17.1

1. Question

Can a match name the empty case of a T?? A T? is a T or nothing (Part I 2.3). ??, ?. and if x != null take it apart where one case is special; where the nothing and several values of the T each have their own answer, the program needs a match, and a match had no pattern for the nothing.

2. Decisions

D1 — null is a pattern; the other arms match the T.

In a match over a T?, null is the arm taken where the value is not set. Where a match has a null arm, every other arm is matched against the T inside: a bare name binds the plain T, a literal compares with the value (0 => over an i64?), a variant names one of the T’s variants, and a guard works as on any arm (null if c =>).

match count {
    null => "unknown",
    0 => "none",
    n if n > 9 => "many",
    n => f"{n}",
}

A position of a tuple narrows on its own (#879, owner, 2026-10-27). In a match over a tuple, a bare name at a position whose type is a T? binds the T once the arms before it have taken every value with null at that position that its own arm would match: an unguarded earlier arm has null there, and at each other position either _ or a bare name, or the same null, variant or literal this arm has there (a literal added by #890, architect, 2026-10-27: like a variant it is one fixed value, so the earlier arm took null at the position for exactly the values this arm meets: after (false, null), (false, why) binds why as the T). A variant or a literal at a T? position ((Color::Red, n)) meets only set values and is matched against the T. A tuple is matched as a struct whose fields are its positions.

match (a, b) {
    (null, null) => "none",
    (null, y) => f"b {y.len()}",   // `y` is a `String`: `(null, null)` came first
    (x, _) => f"a {x.len()}",      // `x` is a `String`: `(null, y)` took the rest
}

Without such an arm the name binds the T? as before: after (null, null) alone, (x, _) still meets (null, "b"). A guarded arm before takes nothing (D2), and no new pattern says “set”: a guard that proves it ((x, _) if x != null, ADR-382 D2) or the null arms before say it.

Why. The principle of ADR-382 D1: where the compiler has proved the value is set, it is a T. An arm after the nothing has been given its own arm meets only values, so the name it binds is a value and needs no ??.

D2 — A null arm and a catch-all are a complete match.

Over a T? with a null arm, the nothing is covered by an unguarded null arm or by else, and the rest is counted against the T as any match over a T is: an unguarded bare name covers it, an enum’s variants are counted, true and false cover a bool. A guarded null arm covers nothing, as no guarded arm does (ADR-291 D11). What is missing is NK1151.

Why. The same rule as every other match, with one case added to it: no second notion of completeness.

D3 — null over a value that is never null is refused (NK1256).

A null arm in a match over a type that is not a T? can never be taken, and is refused rather than ignored. Where the subject’s type is not known, nothing is claimed.

Why. An arm that cannot run is a misreading of the type, and it would also change what the other arms bind.

D4 — Lowered to None and Some(…).

null is None; every other arm but else is Some(<pattern>). In a tuple, null at a T? position is None, and a position D1 matches against the T is Some(<pattern>).

D5 — Some and None are not Nikaia; a pattern that names them is refused.

A T? has no constructors in the language (ADR-278 D5): its nothing is null, its value is the T itself. So Some(x) and None are not patterns of a T?, whatever arms stand beside them; the compiler refuses each with its own code and the help writes the Nikaia form (Some(x) => is x =>, None => is null =>). Over a type of the program’s own that declares a variant called Some or None, the name is that variant’s, as any other.

Why. A pattern the language does not have, taken as an unknown variant, binds an untyped name and passes or fails by accident (Some(x) beside else was accepted, beside null refused as incomplete). One way to write the value of a T? in a match, the one D1 gives.

3. Rejected

B: nullables stay out of match, taken apart only with ??, ?. and if. Where three or more cases meet - nothing, a special value, the rest - it nests an if around a match over x ?? <a value that cannot occur>, whose fallback is dead code that reads as if it mattered (ADR-382, option A).

This site is open source. Improve this page.