T? Is a Type the Compiler Wraps Into, and ?. Reaches a Member Through a View of Its ReceiverStatus: 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: Part I 2.3’s own example, which did not compile (&str? was a
parse error, null a name); the owner’s question whether ?. reaches a
method; the unwrapped return of a value whose type is not known;
#192,
?. takes the value it reaches through; and
the former open-decisions.md’s what does ?? mean when its
left side is a view?, where the owner took option A
Related: ADR-283 D2 and D3 (the tether-state lattice; a copy
is never inserted) ·
ADR-288 (the emitter has no types, so every which shape is the
checker’s) ·
ADR-010 D1 (the fail-closed direction) ·
ADR-298 D1 (null is reserved) ·
ADR-279 D1 (a ??’s fallback is one value, or brackets) ·
ADR-094 D4 and ADR-282 (a value’s state is the
compiler’s per use) ·
ADR-185 D2 (a binding written for the language below) ·
ADR-282 D8 (.clone() is the copy) ·
ADR-279 (what ?? does to an owned or
borrowed left side) ·
ADR-293 (a map of T? values reads one T?) ·
ADR-302 (a C handle may be absent, and is a T?) ·
Part I 2.3 and 3.5, Part III 15.2, C.1, C.4
How does the language write the absence of a value, how does a program reach a member of something that may be absent, and what does that reach do to the thing it reaches through?
Part I 2.3 says types are non-nullable unless a trailing ? says otherwise, and
Part I 3.5 gives two operators: ?. reaches a member only where the receiver
is not null, and ?? supplies a fallback. Each sentence left questions the
compiler had to answer: who writes the backend’s Some(…), what to do where
the value’s type is not known, whether a method is a member, what a plain .
on a T? means, and whether ?. moves its receiver.
| Language | Its answer | Source |
|---|---|---|
| Rust | Option<T>; the constructor Some(x) is written by the program; From<T> for Option<T> and the identity From<T> for T both exist, so x.into() against a declared Option<T> is right whether x is a T or already an Option<T>; Option::map takes its receiver by value, as_ref() borrows it; a reference into a temporary bound by a let is temporary value dropped while borrowed, with the help consider using a let binding |
measured against this repository’s toolchain (D7, D21) |
| Kotlin, C#, Swift, JavaScript | ?. reads through its receiver and leaves it where it was; ?. guards its own member only; ?? / ?: is right-associative |
not re-checked (D14, D15, D16) |
| JavaScript, C# | null inhabits every reference type, so a?.b.c with a.b absent is a run-time TypeError / NullReferenceException |
D14 |
What does not transfer: in those languages a reference is shared and collected,
so reads through the receiver costs nothing to say. Here whether the result is
a copy, a view or owned is a state the compiler solves
(ADR-283 D2), and a copy is never inserted (D5) - so each shape of
?. had to be measured for which state it needs.
| Question | Rejected option | Why it lost |
|---|---|---|
Who writes Some(…) |
the program writes it | there is no Some in Nikaia; the type’s purpose would become paperwork (D5) |
A value of unknown type into a T? |
leave it alone | the program fails in the language below, try wrapping in Some, about a form Nikaia lacks (D7) |
| refuse it | refuses correct programs (C.4) | |
.into() everywhere |
right, but Some(x) says what the source means where the type is known (D6) |
|
?.m() |
refused, as not named by 3.5 | 3.5’s word is member, and a method is one (D10) |
| Lowering a method reach | the field’s map |
an .await does not compile in a closure and a ? has nowhere to go (D12) |
a?.b.c |
read as a?.b?.c |
one ?. would silently govern a chain of any length (D14) |
What ?. does to the receiver |
take it | every language with ?. leaves the receiver (D16) |
| A non-copy member | wait on the tether state (#196) | measured: no ?. shape needs it (D17) |
| refuse it | would refuse find(1)?.name ?? …, which compiles and runs (D19) |
|
guard by emit::is_a_place |
asks whether an expression can be written to; find(1)?.home?.city passes and roots in a call (D19) |
|
A view ?? an owned fallback |
B: leave it | rustc errors about a file nobody wrote (C.1) |
| C: widen to owned | inserts a copy, which ADR-283 D3 bans | |
| A view out of a temporary | hoist the receiver everywhere | on the lazy side of ??, &&, || it would run a call that otherwise does not run (D22) |
T? is a type, and a trailing ? is how it is written.let mut maybe_string: ref String? = null
maybe_string = "World"
It lowers to Option<T> (Part III 15.2’s mapping, the other way round). The ?
comes last: Vec[i64]? is a nullable list, Vec[i64?] a list of nullables.
(A, B)? is a nullable pair (Changed by ADR-364 D1). A diagnostic says String?, never Option[String],
which is a name no program can write.
? is a flag in the AST and a wrapper in the type.In the AST it sits beside the view flag, because a nullable view is both and
the two are independent. In contracts::ty::Ty it is Nullable(Box<Ty>),
because every reader of a type already recurses, so a forgotten case is one the
compiler names.
T stands where a T? is wanted; nothing stands the other way.The one widening the language has. A T? gets back to a T through ??.
null has no type of its own, and is a reserved word.null fits anything nullable and needs no wrap. let m = null with nothing
beside it is not refused: let mut m = null then m = "hi" is correct, and
where nothing ever says the type, the backend asks for the annotation (C.4).
There is no Some in Nikaia. Whether the value is already a T? is a question
about types, and the emitter has none (ADR-288), so the checker
records each wrap and the emitter writes it, at five positions: an annotated
let, an assignment, a return, a struct-literal field and a call argument
(keyed by the statement, the field name, or the callee as written and the
argument’s position).
T?, the wrap is Some(…).Known by its type, or because it is a literal - a number has no type of its own
(Part I 2.4), so return 42 against an i64? would otherwise go unwrapped.
Some(x) stays because it says what the source means.
value.into().Right in both directions: Some(x) for a T, x itself for an Option<T>.
The target is pinned at every position by the declared type, so it is never
inferred from the value. A value already known to be a T? gets neither.
check::wrap_for answers Constructor, Conversion or nothing, and the
emitter writes both forms in one place.
?. reaches a field, and map or and_then is the checker’s choice.let city = find(1)?.home?.city ?? "nowhere"
map over a plain field, and_then over a field that is itself a T?, so
a?.b?.c never reaches through a nullable of a nullable. Decided from the
declared type and recorded per statement and name; a statement where the same
name is mixed falls back to map. The result is a T? either way.
?. reaches a method, with everything a call has.let greeting = find(1)?.greet("Hallo") ?? "nobody"
Arguments, the ; option zone and a trailing lambda reach it; what it throws,
whether it pauses and what it binds are checked by the one function that checks
a plain call. A ?. decides whether the call happens, never what a call is.
A method whose own result is a T? gives a T?, not a T??.
match, because a method may pause and fail.match find(1).as_ref() {
Some(__nikaia_it) => Some(__nikaia_it.read("note.txt").await?),
None => None,
}
Inside a closure an .await does not compile and a ? has nowhere to go. The
flattened case drops the Some(…) around the arm.
?. through a value that cannot be absent is NK1121.Changed by ADR-279 D13: refused only through a list’s index; elsewhere the warning NK1217, and the ?. is the plain ..
Only where the receiver’s type is known (C.4). The help keeps the spelling the
reader wrote: write .name, or write .name(…) for a call.
. on a T? is NK1125.a?.b.c guards a and nothing else; with a.b absent, .c has nothing to
reach on. Where null inhabits every reference type that is a crash at run
time; here T? is a type of its own (Part I 2.3) and a member of T is not a
member of it, so it is refused where it is written. The way out is ?.c, or
?? and then the plain .. maybe().c, with no ?. at all, is the same
refusal.
?? is right-associative.a ?? b ?? c is a ?? (b ?? c): the last fallback ends the chain, and every
?? before it takes the T? on its left.
?. takes nothing: the receiver stays usable.user?.name reads user where it lies; user is usable on the next line, in a
second ?., in a call.
Where the member does not copy, the result is a view of the receiver, kept
alive as any view of a place is (Part I 6.6). Its state is Borrowed
(ADR-283 D2’s free case): no shape of ?. escapes the receiver’s
scope, so none needs the tether - a local receiver, a temporary consumed in the
statement and view ?? view were measured to compile and run Borrowed at zero
cost. Tethered stays for what escapes - a container
of views returned past its buffer’s scope, a parser’s rows handed past it, a
kept lambda capturing one. A program that keeps the result past its
receiver writes the copy, .clone(); nothing is copied that it did not write.
x.as_ref().map(|it| it.a), with
and_then where D9 flattens.mutates column):
D12’s match over x.as_ref(). What comes out is the call’s own result, not
a view of the receiver.A field whose type the checker could not work out lowers taking the receiver - the direction that cannot break a program that worked (C.4).
check::roots_in_a_binding follows the chain - a name, a field or index of one,
a ? or cast over one - to its root. Rooted in a binding, the member is a view
(.as_str() or &, .as_deref() or .as_ref() where it flattens). Rooted in
a call, the reach takes the temporary, which has no next line to stay usable
on, so D16 loses nothing there. It is not refused: find(1)?.name ?? "nobody"
compiles and runs.
?? joins two views; a view beside a fallback that owns is NK1185.An owned result would copy the borrowed branch (ADR-283 D3’s ban), and a view
result needs the fallback to be one, which a text literal already is. So the
mixed form is refused, and the help is ?? "…". A copy spelled by name
(.clone(), .to_string()) counts as owned even where no ledger types it.
Both sides have to be known (C.4).
let a = find(1)?.label() ?? "none" // label(ref self) -> ref String
The emitter binds the receiver on the line before and the reach reads the binding, which lives to the end of the block as a name the program bound would. Nothing is copied, and nothing runs that did not: the call ran first in that statement anyway.
Not on the lazy side of ??, && or ||; not in a match arm; not after
another call in the same statement; not where the view is the block’s value or
the function’s result. There the program is refused in this language’s words,
naming the let it can write.
Where a reach still takes its receiver (D18’s unknown field, D19’s temporary),
the backend’s Option::map takes ownership is translated to ?. over a
member that is not copied takes the value it reaches through; it never names
.as_ref() or a clone the program did not ask about.
T?.?. and ?? read as they do in the languages they come from, minus the
run-time null crash (D14) and the move (D16)..into() where the compiler could not work a type
out, and a match where a method is reached - the price of D7 and D12 against
ADR-004 D2.?. waits on the tether; what does is listed under D17.T? crosses into foreign code as its T does, since it lowers to
Option<T>; a nullable C handle is ADR-302’s.?. on the left of an assignment (user?.name = …): the language does not
have it.??, which would need the binding filled only when
that side runs; D22’s refusal names the way out..into() should become the only wrap form (D6 keeps Some(…) until a
measurement says otherwise), and which std entries the ledger should gain.??’s precedence against || and the comparisons beyond
ADR-279 D1.null is a value or an absence at run time: it is None, and nothing
here depends on a difference.