let Takes Names or a Flat Tuple, _ Ignores a Value That Arrived, and a match Takes Nested Patterns, Ends in else, Covers Every Case and Lends What an Arm Only Reads — With .. Inclusive and ..< Exclusive EverywhereStatus: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: #224
(let takes one name, and the specification writes it taking several),
#209 (_ is a name,
and let _ = f() lowers to Rust’s discarding let _ =),
#218 (match lacks a
tuple, an or-pattern, a range, a guard, a nested pattern and a struct’s rest),
whether a match must cover every case, the question what a
catch-all arm is written with, and
#168 (a match that
binds a field of a name moves the name)
Related:
ADR-292 (overlap { … }, whose tuple a let takes apart);
ADR-149 (a channel’s (tx, rx), the other site of D1);
ADR-037 D6 and D7 (the sharing floor a destructure leaves alone);
ADR-279 D2 (the parse-error note D5 copies);
ADR-094 D2 and D4 (a copy is the program’s to write; a let over
a place is a view, which D8 keeps);
ADR-277 (function types, whose lambdas D6 lets ignore an argument);
ADR-276 and ADR-298 D1 (what a keyword costs);
ADR-132 (else if, and what else reads as);
ADR-140 (one meaning, one spelling);
ADR-293 (refines D13: a range is a value that may be named and is
walked as a sequence);
ADR-282 (follows D14’s shape: a withdrawn spelling is refused
naming its replacement);
ADR-023 D1 and ADR-024 D1 (an error type is an
enum; the ledger, and the absence of a claim);
ADR-203 (an enum’s variants in the ledger, so D19 reaches a
package’s enum, and a match over a catch’s error);
ADR-280 (a generated sum, covered by D19’s rule);
ADR-293 D32 (what is handed over, which D23 reads);
ADR-282 D7 (no copy the program did not write);
ADR-246 (a match through a boxed part);
Part I 2.1, 3.3, 3.4, 6.5, 8.1.2; Part II 12.5; Part III C.1 and C.4
What may a program write where a value is taken apart — at a let and at a
match arm — and what does each form promise?
Six questions meet at that place:
let (user, rights, prefs) = overlap { … } and Part II 12.5’s
let (tx, rx) = … need a let that takes more than one name.let _ = does.match needs more than a literal and a path with bindings (examples/calc
wants to match step, not step.0). A range pattern brings a second
question: Rust’s inclusive pattern is ..=, and .. must mean one thing.match missing a case must be refused in this compiler’s words, not
rustc’s on a Nikaia line, since Part I 3.4’s first sentence promises every
case is handled.match s { Shape::Named(n) => … } must not move the String out of s where
the arm only reads it, or a second read of s is a correct program refused.They are one record because they are one construct read from two ends: what a pattern may be, which character means what inside it, when a set of arms is complete, and what a binding takes.
| Language | Its answer | What transfers | Source |
|---|---|---|---|
| Rust | Full patterns at let and match; _ is the wildcard and let _ = x drops a temporary at once, which is how a guard’s release is timed; _name silences the unused warning; a..b exclusive, a..=b inclusive, and only ..= in a pattern; a guard is if; a non-exhaustive match is refused; a match on a place binds by value and moves |
The lowering of every shape here is Rust’s, verbatim; the coverage rule is the same, so the two answers agree. Not the word wildcard, not let _ =, not the range spelling, not moving by default |
D1, D7, D13, D19, D23 |
| Kotlin, Swift | a..b includes its end and a..<b excludes it |
The pair D13 takes, read the same way | D13 |
| Every neighbouring language | A guard is if |
D11 | D11 |
| Question | Rejected | Why it lost |
|---|---|---|
How much a let takes apart |
A pattern language at let |
match has patterns; neither site needs them, and a bigger answer is a decision rather than the repair (D1) |
| A second statement variant | Additive and cheaper, and a walk that has to remember it is a walk that will forget it (D2) | |
| Comparing a tuple’s width with the names | A value this compiler cannot see is ?; refusing it would refuse a correct program (D3) |
|
Ignoring let (a, b): T |
It drops something the author wrote (D4) | |
What _ is |
A name, as it was | let _ = f() then means discard below and bind above |
Rust’s let _ = expr to time a drop |
The one thing it is used for is the one thing a reader cannot see in it; a resource is closed by name or ends with its scope (D7) | |
_name for bound but unused |
No warning to silence; a name not meant to be read should not be there (§6) | |
| A guard’s word | where |
A second word for a condition, ADR-276’s cost for nothing (D11) |
| A range pattern | Rust’s ..= in a pattern, .. exclusive elsewhere |
A reader from Rust looks for ..=, and 1..5 would mean one thing here and another there |
.. exclusive in an expression, inclusive in a pattern |
Both forms parse in both places and mean different things; the compiler cannot help a reader hold the rule (D13) | |
Keeping ..= beside an inclusive .. |
Two spellings of one range, the defect ADR-140 removes (D14) | |
..< in a pattern |
A pattern stays one shape; the end is moved instead (D13) | |
| Migrating over several changes | The old spelling keeps parsing and changes meaning, so the corpus would mean something nobody wrote in between (D15) | |
| The catch-all arm | _, as before |
Nothing arrives at that arm, so the ignore pattern’s character says the wrong thing; two spellings would be ADR-140’s state again (D16) |
| A new word | else is reserved and already means the branch taken when nothing before it matched (D18) |
|
| Coverage | Leaving it to rustc |
The reader gets the backend’s words on a Nikaia line, Part III C.1’s class (D19) |
| Refusing where the type is unknown | A refusal on a guess is a correct program refused, C.4’s worse mistake (D21) | |
| An arm’s bindings | match &s |
Every binding a reference, numbers too: r > 0 compares &i64 with i64, and the arms need a * the source does not write |
match s.clone() |
A copy the program did not write (ADR-282 D7) |
The number that decided: no .nika file in examples/, tests/ or
crates/nikaia-std/ wrote a bare _ or a ..= range, which is what made D7 and
D14 free.
let takes one name, or a flat tuple of names — not a pattern language.Changed by ADR-356 D1: a let and a for head take a nested tuple of names and _; still no variant.
let (user, rights, prefs) = overlap { … } // Part I 8.1.2
let (tx, rx) = channel::bounded(100) // Part II 12.5
What the two sites need is taking apart a tuple whose arity is known; match
is where patterns are.
let, read by every reader of the tree; an analysis that cannot answer for several names declines.A let holds a list of names, so every walk of the tree has to say what it does
with several. The checker binds each name to the part at its index, or to ?;
the emitter writes Rust’s tuple pattern; views marks every name a carrier.
Three decline, each on the safe side: contracts::order goes opaque rather than
reorder a statement binding several names, contracts::sharing records no
handle so each part stays at ADR-037 D6’s atomic floor, and dsl
skips, because a deferred block is one value.
Changed by #704: where the width is known for certain - a tuple of another width, or a value that is certainly no tuple - the pattern is refused, NK1283.
A value whose type this compiler cannot see is ?, and refusing a destructure
of one would refuse a correct program (Part III C.4).
The day a tuple’s width is known for certain is the day to refuse it here.
let is refused, NK1136.let (a, b): T = … cannot say which name T is about; refused rather than
ignored.
let is refused with a sentence that names the shape.Changed by ADR-356 D1: a nested tuple is taken apart; a variant in the head is NK1259 (D2).
It does not parse, and the parse error carries a note saying that the shape is what is missing, recognised from the rendering as in ADR-279 D2.
_ is the ignore pattern, and it stands where a name would be bound.let (name, _) = pair() // a position of a destructured tuple
fn handle(event: Event, _: Context) { … } // a parameter a shape dictates
xs.map fn(_, value) { value * 2 } // a lambda's argument
match point { (0, _) => "on the axis", else => "off it" } // a part of a pattern
_ means ignore a value that arrived — neither bind it nor use it — and is
not called a wildcard. It is not a name: _count, _0 and the _000 of
1_000 still are. Two _ in one list are fine, and a parameter written _
still carries its type. The whole match arm is not one of its positions:
that arm is else (D16).
let _ = expr is refused, NK1144, and _ is never a value.A call made for its effect is written as the call; a resource is closed by name
or ends with its scope (Part I 6.4). x + _ does not parse.
_ ignores is not moved, and a temporary it belonged to ends with the statement.let (a, _) = place binds a and nothing else, so ADR-094 D4’s
view rule holds: where a is only read, the place is not moved.
_ lowers to Rust’s own _, so no binding exists below to warn about.An ignored parameter is _: Context in the ledger’s signature column, and its
keeps answer is never kept, so a caller lends it.
match takes six more shapes: a tuple, an or-pattern, a range, a guard, a nested pattern, and .. for a struct’s rest.match point {
(0, 0) => "origin", // a tuple
(0, y) | (y, 0) => "on an axis", // an or-pattern
(x, y) if x == y => "diagonal", // a guard
else => "elsewhere",
}
match code {
200..299 => "ok", // a range, inclusive at both ends
else => "other",
}
match event {
Event::Click(Point { x, .. }) => x, // nested, and `..` for the rest
else => 0,
}
A tuple pattern’s parts are patterns, so nesting is recursion. Every
alternative of an or-pattern binds the same set of names, NK1155, because the
arm’s body does not know which one matched. The guard stands on the arm, not on
an alternative.
if... in a pattern is inclusive at both ends.200..299 matches 200 through 299: a pattern is a set of values, with no
counting to n in it.
.. is inclusive everywhere, and the exclusive range is ..< — in a for, a slice and anywhere else, never in a pattern.for i in 0..<n stops at n - 1. One meaning per spelling, after Kotlin and
Swift. A pattern that wants up to, not including moves its end: 200..298.
..= is refused, naming 0..n... says what ..= said; keeping it would be two spellings of one range.
Every a..b that meant the exclusive range became a..<b at once, because the
old spelling keeps parsing and changes meaning.
match’s catch-all arm is else, and _ in that position is refused, NK1150.match code {
200 => "ok",
404 => "not found",
else => "other",
}
The message names the replacement.
_ keeps every other position: a tuple’s, a parameter’s, and a part inside a pattern.These are the positions where a value did arrive (D6).
else costs no word, and lowers to Rust’s _.It is reserved already and already reads as the branch taken when nothing before it matched (ADR-132).
match covers every case, and the refusal is this compiler’s, NK1151.A match over an enum is complete when every variant is named, and needs
no else; an or-pattern covers one case per variant it names. Over anything
else it needs an else (or a bare name, D20), because the set is not
enumerable in arms — bool is written out as the case that is neither: true
and false are a complete match. A guarded arm covers nothing, since the
values its guard turns away reach the arms below. NK1151 names what is
missing: the variants, or else.
match op {
Op::Plus => a + b,
Op::Times => a * b, // complete: the enum has two variants
}
other => … binds and matches anything, read by the same rule as else.
The backend’s answer stands until the type is known (Part III C.4). This is the one place the rule is weaker than it reads.
A name, or a field of one, that is not itself a view: a positional variant’s parts in order, a variant with named fields by name, a library’s variants from its ledger entry. A temporary and a view’s parts are unchanged. A binding the variant does not type — inside a nested pattern or an or-pattern — stays as it was.
match.It is ref n below. The arm keeps the part — and takes it — where it hands
it to something that keeps it (a push, a field, a rename, a callee whose
keeps says so, ADR-293), calls a method on it that changes or
takes it, assigns to it, or returns it or hands it back as the arm’s value. A
number, a bool and a char are copied as before.
A method on a bound part resolves, so what it pauses, fails with and touches is counted where it was unknown.
field: pattern, and field alone is field: field.Point { x: 0, y } matches where x is 0 and binds y; Move { x: px, .. }
binds the field x to the name px; Shape::Circle { center: Point { x, .. }, .. }
reaches inside. The part after the : is any pattern, read as D10’s parts are:
a bare name binds, a path with :: names a variant, a literal is a value.
A variant’s parts are destructured the same way whether they have names or
positions: a tuple’s parts are patterns (D10), and so are a struct’s fields. The
shorthand alone left a struct variant’s fields unable to be tested against a
value or reached into, so Expr::Binary { op: BinaryOp::Add, lhs, rhs } had to
be a guard or a second match. It also could not bind two fields of the same
name in one pattern ((Pair { name: a, .. }, Pair { name: b, .. })), and an
or-pattern over differently named fields could not bind one name for both
(Shape::Circle { r: size } | Shape::Square { side: size }, D10’s NK1155
rule). Every language with record or struct patterns has this form: Rust
({ x: px, y: 0 }), OCaml ({ x = px; y = 0 }), Haskell, Scala and Python
(Point(x=px)), C#, JavaScript ({ x: px }). It lowers as Rust writes it.
Rejected:
let in the arm. It covers
renaming, not testing or reaching into a field, and not the two cases above.field: name). A second rule for the part after the :
where D10 already has one.The cost: in a pattern, x: px reads the other way round from a struct literal,
where x: px gives the field the value px. Rust and JavaScript have the same
asymmetry.
let over a tuple reads the parts by
name instead of r.0._ knows a value arrived and was ignored on purpose; one
who writes let _ = f() is told to say what they meant. A lambda handed to a
two-argument function type can ignore one without inventing a name... has one meaning, at the cost of a character on the exclusive range,
which is the form written most.match and if say otherwise with one word, and _ has one reading.enum that gains a variant makes every match over it without an else
a refusal here, across a package boundary too, since the ledger carries
the variants (ADR-203). The cost is one walk per match.match over a name that only reads leaves the name usable; a part kept is
taken, as any hand-over is.let read by every walk; the ignore
pattern in four positions; the pattern rules, the binding-set rule and the
range spelling; the refusals NK1136, NK1144, NK1150, NK1151,
NK1155 and the ..= and ..<-in-a-pattern refusals.let (let (a: i64, b) = …):
written nowhere, and a decision when it is.contracts::order and contracts::sharing decline (D2): deliberate
holes on the safe side, closed by carrying several names, not by a new rule._name, bound but deliberately unused.@ (n @ 1..5), a seventh shape, not decided here.else covers meanwhile.else with a guard (else if x > 0), which nobody writes and a guarded
pattern covers.