T? Is a Type, null Is a Word, and the Some(…) Is the Compiler’s to WriteStatus: Superseded by ADR-278 — its D1-D3 are D1-D3, D4 is D5-D6, D5 is D4, D6 is D9, D7 is D13, D8 is D16 there
Related: Part I 2.3 (the rule this makes true) and 3.5 (??, which was
already built and had nothing to be built on), Part III 15.2 (the mapping, Rust
Option<T> to Nikaia T?), ADR-051 D1 (null joins the reserved
list), ADR-024 (the ledger, which writes T? and not Option[T]),
ADR-028 (why the wrap is decided in the checker and written in the
emitter), ADR-010 D1 (the fail-closed direction the wrap rule takes)
Part I 2.3 says types are non-nullable by default and that a trailing ? allows
the absence of a value. Neither line of its own example was accepted: &str?
was a parse error, and null was read as an ordinary name.
And the state was worse than “unbuilt”, because Part I 3.5’s ?? was built —
a ?? b lowered to a.unwrap_or_else(|| b.into()), an operation on an
Option in a language with no way to write one. So the only values it could be
used on came out of std, and the type the whole section is about could not be
named by a program.
T? is a type, and a trailing ? is how it is written.let strictly_string: String = "Hello".to_string()
let mut maybe_string: String? = null
maybe_string = "World".to_string()
It lowers to Option<T>, which is the mapping Part III 15.2 already wrote the
other way round. The ? comes last, after the type’s arguments, so
Vec[i64]? is a nullable list and Vec[i64?] is a list of nullables.
Option is what it lowers to and not what it is called. A diagnostic says
String?; Option[String] is a name no program can write, and a message that
used it would be a message about the generated file (Part III, C.1).
? is a flag in the AST and a wrapper in the type.Two representations of one thing, on purpose.
In the AST it sits beside is_view, because &str? is a nullable view and
the two are independent: a wrapper there would have to be unwrapped by everything
that asks whether a declaration names a borrow.
In contracts::ty::Ty it is Nullable(Box<Ty>), because every reader of a type
already recurses — so a wrapper is one arm per reader instead of a condition
inside each of them, and the compiler names the readers that forgot.
T stands where a T? is wanted. Nothing stands the other way.This is the one widening the language has, and Part I 2.3 is what asks for it:
maybe_string = "World" hands a String to a String?. The other direction is
refused, because that is the whole point of the type being separate — and ??
(Part I 3.5) is how a program gets from a T? back to a T.
Some(…) is written by the compiler, and the checker says where.The language below needs the constructor. There is no Some in Nikaia and must
not be: a program says maybe_string = "World", and a wrapper the programmer has
to write would make the type’s whole purpose into paperwork.
Whether the value beside the = is already nullable is a question about
types, and the emitter has none (ADR-028). So the checker records
where a wrap is needed and the emitter writes it — the same arrangement, for the
same reason, as shared_sites (ADR-040 D1) and the fallible loops
(ADR-025 D1).
Four kinds of position, and each is keyed by the narrowest thing that identifies it, because an expression carries no span:
| where | key |
|---|---|
an annotated let, an assignment, a return |
the statement |
| a struct-literal field | the statement and the field’s name |
| a call argument | the statement, the callee as written, and the position |
The written callee and not the resolved one, because the emitter has only what
the source says: a method’s key is Type::method and a constructor’s is
Type::new, and neither stands at the call.
Two ways a value is known not to be nullable already, and a type is only the first:
T?;return 42 against a
declared i64? answers unknown — and the type alone would leave the
commonest case in the section unwrapped.Everything else is left alone, which is the fail-closed direction
(ADR-010 D1): wrapping a value that is already an Option<T> would
make an Option<Option<T>>, so the set never claims a wrap that is not needed.
null has no type of its own, and is not refused for it.null answers “a T? of it-does-not-say”, so anything nullable fits it and
nothing needs wrapping around it.
let m = null on its own is not refused here, and that is C.4 rather than an
oversight: let mut m = null followed by m = "hi" is a correct program, and
this checker has no inference to tell it from the one where nothing ever says. So
rustc asks for the annotation, which is the honest answer — this compiler cannot
say what the value is the absence of, and neither can the program.
null is a reserved word (ADR-051 D1), which is what keeps the
other reading away: as a name it would be NK1117, and as a name something
declared it would be a different program.
?. reaches a field only where there is something to reach it on, and map or and_then is the compiler’s choice.let city = find(1)?.home?.city ?? "nowhere".to_string()
x?.a lowers to x.as_ref().map(|it| …) where a is a plain field, and to
x.as_ref().and_then(|it| …) where a is itself a T? (the as_ref() is
ADR-113’s, and the member is copied or viewed as D8 says). That second one is the
whole difficulty: map over a nullable field would give an
Option<Option<T>>, and a?.b?.c would come out reaching through a nullable of
a nullable.
Which of the two is a question about the declared type, so the checker
decides and the emitter writes the word — the same arrangement as D4’s wrap and
for the same reason (ADR-028). The key is the statement and the
field’s name, which is the shape fallible_methods already uses: an expression
carries no span, so a pair is recorded only where every ?. of that name in
that statement flattens, and a mixed statement falls back to map — the answer
that cannot make a nested option out of a plain field.
The result is a T? either way, which is what makes ?? the way out of a chain
and lets ?. be written again after it.
?. through a value that cannot be absent is refused.NK1121, and the way out is the plain .:
error: `?.` reaches through a `User`, which cannot be absent
4 | let n = u?.name
^
= `?.` exists for a nullable type — it reaches the field only where there is
something to reach it on, and answers `null` otherwise (Part I, 3.5). A
type that is not `T?` always has a value
help: write `.name`
Only where the receiver’s type is known. One this checker could not work out says nothing, because refusing there would refuse a correct program (Part III, C.4).
?. reaches through a view, and the receiver stays usable.x?.a reads x where it lies — x.as_ref().map(…) below — and what comes
out is a copy where a copies and a view of x otherwise, kept alive as any
view of a place is (ADR-113). A receiver used again after a
?. is an ordinary program, and there is no move for a message to explain.
Part I 3.5’s ?? has something to be used on, and needed no change: it
already lowered to unwrap_or_else, and the type it operates on is now writable.
The ledger writes T?. A signature comes back as (id: i64) -> i64? — the
program’s own spelling, in the file a consumer reads.
A T? crosses into foreign code exactly as its T does (ADR-045),
because that is what it lowers to: an Option<T> is Send when T is, and holds
one T or nothing.
(A, B)? is deliberately absent. Part I 2.3 does not write it, and a form the
emitter would have to invent a lowering for is worse built than unbuilt.
?.m() — safe navigation onto a method. Part I 3.5 writes a field in
its example and that is what D6 builds, so this was not left as work: a
form the specification does not name is not this compiler’s to add, and it was
refused with a sentence. “Whether the language should have it is the owner’s
question and not this record’s” — and the owner answered it: the section’s own
word is member, a method is one, and ADR-066 is the record.?. chain should avoid the move. D8 takes the move, and the
alternatives each cost something the language has not decided: as_ref()
changes the result to a view, and a clone is the one thing this language
never does without being told (Part I 6.1). Left as it is, with a message that
says so.null is a value or the absence of one at run time. It is None,
and nothing in this record depends on there being a distinction.D1 through D5. T? parses and lowers, null is a word, and the Some(…) is
written at an annotated let, an assignment and a return.
crates/nikaia/tests/nullable.rs compiles what it lowers rather than
comparing strings, because whether the wrap landed in the right place is settled
by the language below and reading the emitted text would only say that this
compiler agrees with itself. It covers Part I 2.3’s own example, a nullable
result with the wrap at a return, a value that is already nullable not being
wrapped twice, the ledger round-trip, D3’s rule in both directions, and a
diagnostic that says i64? and not Option[i64].
?. is covered too: map over a plain field, and_then over a nullable one,
the struct-literal wrap, NK1121, that an unknown receiver says nothing, and
that a ?? b is not a?.b — the grammar spells ?. as one token, so the
generator cannot put the implicit whitespace inside it and a ? . b is not safe
navigation either.
The harness runs the checker as well as rustc, which was added after it
caught the test author: a ?. on a plain value reached the backend and came back
as “User is not an iterator”, reporting the wrong thing about a program this
compiler would have refused.
The wrap reaches all four kinds of position, the call argument included:
shown("here".to_string()) into a String? comes out shown(Some(…)), and
pair(1, 2) into two i64? parameters is told apart by position.
Nothing of this record is unbuilt. What §4 lists is one form the specification does not name and two questions it does not put.