_ Is the Ignore Pattern — It Stands Where a Name Would Be Bound, and Nowhere ElseStatus: Superseded by ADR-291 — its D1-D4 are D6-D9 there, D1 as narrowed by ADR-145 D1
Date: September 19, 2026
Target Version: Nikaia 0.0.8
Related: Part I 2.1 (names), 3.4 (match, where _ already stands),
5.3 (the one lambda form), 6.4 (Cleanup), ADR-098 (a let
takes one name or a flat tuple of them), ADR-094 D4 (a let
over a place is a view of it), ADR-102 (a function type, whose
lambdas have to fit a given shape), Part III C.1
_ exists in one place: a match arm, where it matches anything and binds
nothing. Everywhere else a value arrives it has to be given a name, whether
or not the body wants it — a tuple’s second half, a parameter a trait or a
callback type dictates, a lambda’s argument a map hands over. A name nobody
reads is a name a reader looks for, and in the language below it is an
unused variable warning about a file nobody wrote, which is the class
Part III C.1 forbids.
The word matters. Other languages call _ a wildcard, which suggests a
type that accepts anything. That is not what it does here: it is an
instruction to ignore a value that arrived — neither bind it nor use it —
and the name should say so.
_ is the ignore pattern, and it stands where a name would be bound.Three positions, and only these:
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 code { 0 => "ok", else => "other" } // see below: this one moved
A tuple position, a parameter of a function or a lambda, and — until
ADR-145 — a match arm. That one is else now, for this record’s
own reason: _ means ignore a value that arrived, and nothing arrives at the
arm taken when none of the others matched. The two positions above it stay.
Two _ in one list are fine — nothing is bound, so nothing collides — and a
parameter written _ still carries its type, because the caller needs it.
let _ = expr is refused, and _ is never a value.A binding that ignores its whole value is a statement wearing a let: a call
made for its effect is written as the call, f(). A resource that has to be
torn down at a moment of the program’s choosing is closed by name, close(),
or lives in a scope it can end with (Part I 6.4) — never by a let whose
only purpose is to decide when a Cleanup runs, which is the one thing
Rust’s let _ = is used for and the one thing a reader cannot see in it. The
refusal is NK1144: _ ignores a value inside a pattern; write the
expression as a statement, or bind it to a name.
_ in an expression (x + _) does not parse, as it never did.
let (a, _) = place binds a and nothing else, so ADR-094
D4’s rule holds unchanged: where a is only read, the let is a view of the
place and the place is not moved. The value an _ ignores in a destructured
temporary is released at the end of that statement, which is what a value
nobody bound would have done anyway. Nothing about the doors changes: there
are no lock guards to release early, because there are no first-class lock
guards.
_, so no binding exists to warn about.In a tuple pattern and a parameter list the emitter writes _, which binds
nothing below either; a match arm already did. The unused variable
warning that a placeholder name used to earn in the generated file cannot
arise, which is the C.1 point.
_ knows a value arrived and was ignored on purpose;
one who sees let _ = f() is told to say what they meant._: Context in the signature
column, and its keeps answer is never kept, so a caller lends it._ in two more positions of the grammar, one refusal,
and the emitter passing the pattern through._name, meaning bound but deliberately unused. Rust has
it for the warning; this language has no such warning to silence, and a
name a reader is not meant to read is a name that should not be there._ in a struct pattern (Point { x, .. }), which is the .. rest of a
later record and not this one.All of it, and two of this record’s own sentences were wrong about the state
it was correcting: §5’s let _ = f() does not parse either and D2’s x + _
does not parse, as it never did. Both parsed. _ was an ordinary name, so
let _ = f() compiled and lowered to Rust’s own let _ =, which discards the
value where the source said bound, and x + _ was a name NK1117 said nothing
declared. That is the defect #209 recorded and this record answers, and
it is why the work below is not only an addition: a character had two readings and
the compiler’s answer was an accident of what the parser happened to accept.
_ is no longer a name: NAME rejects a bare one, so x + _ does not parse and
the ignore pattern is producible in exactly the three positions D1 names — a
tuple position, a parameter of a fn or a lambda, and the match arm it always
had. The symbol it carries is one no NAME can produce, which keeps anything else
in the compiler from mistaking a name for it.
Two lookaheads in front of it, and they are what _name costs: a bare _ is
one followed by neither an identifier nor a digit. So _count and _0 stay
names, and so does the _000 of 1_000 — which matters, because NK1117’s
a number is written in digits with no separators is a message about that name.
let _ = expr is accepted by the grammar and refused by the checker
(NK1144), which is the only arrangement that can carry D2’s sentence: a refusal
with a message has to come from somewhere that can write one, and a parse error
at a character cannot. let (a, _) = pair() is D1’s tuple position and binds a.
The lowering is Rust’s own _, and the ledger writes _: Context in the
signature column, where its keeps answer can only be not kept — a body that
never names a value cannot store it — so a caller lends it and the parameter is
&Context below. §3’s third bullet, arrived at rather than arranged.
Seven tests in crates/nikaia/tests/ignore_pattern.rs, and one of them is D4
through rustc itself: the emitted program is compiled without -A warnings,
because the unused variable warning is what is being asserted absent and
silencing warnings would make that test pass forever. Another keeps
#209’s own finding — that nothing in examples/, tests/ or
crates/nikaia-std/src/ writes a bare _ — which is what made this free to
decide and is now what makes it free to keep.