Status: Superseded by ADR-278 — its D1-D3 are D7, D6 and D8 there Extends: ADR-052 D4, which decided that the compiler writes the wrap and the checker says where — and left one answer out Related: ADR-028 (the emitter has no types, so the question is the checker’s), ADR-020 and ADR-028 D5 (the ledger is thin on purpose, which is why “the type is not known” is a permanent state and not a temporary one), ADR-010 D1 (an analysis that fails open is a vulnerability generator — the caution this keeps while removing its cost)
ADR-052 D4 gives the Some(…) to the compiler: a plain T
standing where a T? is wanted is Part I 2.3’s one widening, and the constructor
is not a form Nikaia has. Four positions carry it — an annotated let, an
assignment, a return against a declared result, and a struct literal’s field —
and a call’s argument is the same rule written a fifth way.
The rule writes it only where the value is known not to be a T? already,
because wrapping one that is would make an Option<Option<T>>. That caution is
right. What it did with the rest was stay silent, and this is what that
costs:
impl U {
fn a(&self) -> String? { return self.name.clone() } // no wrap
fn b(&self) -> String? { return "lit".to_string() } // Some(…)
}
fn c(u: &U) -> String? { return u.name.clone() } // no wrap
fn d() -> String? { return "lit".to_string() } // Some(…)
Checked::nullable_sites held two of those four. What separates them is
.clone(): no ledger describes it, so the value’s type is ? and the rule
declines. A reader then gets rustc about the generated file — “expected enum
Option<String>, found struct String“, with “try wrapping the expression in
Some” about a form this language does not have, which
Part III C.1 calls a bug in this compiler.
And it was first filed under the wrong cause. The entry read “a return in
an impl method does not get its Some(…)” — the position, which is not it:
b is in an impl and is wrapped, c is free and is not. Measuring the four
shapes is what replaced a plausible cause with the real one.
Why this does not go away by itself. “The type is not known” is not a gap waiting to be filled in. The ledger is thin on purpose: an entry exists because a program asked for it (ADR-028 D5), and a language that lowers onto another one’s library will always have names nothing has written down yet. So the silent position is permanent, and so is the class of programs it refuses.
value.into().self.name.clone().into() // the type is not known
Some("lit".to_string()) // the type is known
It is correct in both directions, which is the whole of the argument. Rust
has impl<T> From<T> for Option<T> and the blanket identity impl<T> From<T> for
T, so x.into() against a declared Option<T> gives Some(x) where x is a
T and x itself where x is already an Option<T>. The question the checker
could not answer therefore stops needing an answer — not by being guessed at, but
by being made irrelevant.
The target is pinned in every position it is written. A declared result, an annotation, an assignment’s left side, a field’s declared type, a parameter’s — all five name the type the conversion resolves against, so it is never inferred from the value alone.
Some(…) is what the generated Rust goes on saying wherever the compiler knows
enough to say it. The conversion is what uncertainty costs, not what every
program pays — which matters because ADR-004 D2 asks that the text
a person reads be the text that is compiled, and Some(x) says what the source
means where x.into() only does what it means.
A literal counts as known, unchanged: a number has no type of its own on purpose
(Part I 2.4), so return 42 against a declared i64? answers ? and the type
alone would leave the commonest case in the section unwrapped.
The rule was four copies of the same three conditions, and the copies had already
drifted in what they did with an unknown type. It is one function now
(check::wrap_for), and what it hands back is a Wrap — Constructor,
Conversion, or nothing at all for a value that is already a T? and needs
neither.
The emitter asks one function in turn for what goes before a wrapped value and what goes after, so the two forms are written in one place rather than five.
Programs that did not compile now compile, and none that compiled stops. The sites the checker was already sure about are byte-identical; the change is entirely in the positions that used to produce nothing.
The .into() is visible in the generated Rust, and that is the price. A
reader comparing the two languages sees a conversion where the Nikaia says
nothing — the same pressure on ADR-004 D2 that
ADR-055 §2 D6 names for async fn, and smaller: it stands only
where the compiler could not work the type out, and it is honest about that.
It makes the ledger’s thinness cost less rather than hiding it. A clone
nobody has written down is still a type this checker cannot see, and --trust
and the contracts work are still what improve that. What changes is that the
consequence is no longer a program that does not build.
.into() should be the only form. It is correct everywhere, so
Some(…) could go — D2 keeps it because it reads as what the source means, and
the day a measurement says the two forms cost more than they are worth is the
day to revisit it.std entries the ledger should gain. String::clone is the one this
record met, and adding it is worth doing on its own terms
(ADR-028 D5); it is not this fix, because the next program
reaches the same position under a different name.All three. check::Wrap and check::wrap_for are D1 and D3; the four
positions record which of the two they need, and Emitter::around writes it.
tests/nullable.rs runs the decisive program rather than reading it: one
file whose two methods both have values this checker cannot type, where one is a
plain String and the other is already an Option<&str> — .clone() and
strip_prefix, neither in any ledger. .into() is the wrap for the first and
the identity for the second, and the program prints Ada, da and no prefix.
Neither method compiled before, and Some(…) would have been wrong for the
second.
A second test holds D2 from the other side: a known type still emits
Some("lit".to_string()), in an impl and in a free function alike, so the
conversion stays the exception.