T? and a Key Is Not a Position; a Sequence Says What It Is as a Whole; and What Is Handed Over Is Gone, Part by PartStatus: 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: #261
(a map read through the brackets aborts on an absent key);
#208 (collect() into
a declared map passes the checker and fails in rustc); open-decisions.md’s
what a read of a map of T? values is, met moving the sharing analysis
into Nikaia (#125); and the owner’s decisions on outside feedback: the repairs
first (a range in a name, a named keys(), a sequence taken by a let, a
loop or a lambda all reached rustc), reversing a pipeline must not force a
collect(), repair the owned-key map, and fix the open points those
repairs left
Related:
ADR-080 D2 (a write through the brackets is an insert);
ADR-278 (T? is a type, null a word, ?. reaches through one);
ADR-105 D1, D2, D4 (Seq[T], its words, the once-only rule
NK2702, no Seq in a program’s type grammar);
ADR-291 D13 (a range is an ordinary expression and may be named);
ADR-094 D2, D4 (keeps; a for lends);
ADR-283 D3 (the compiler inserts no copy);
ADR-296 D17 (the emitter does not know types);
ADR-278 D20 (?? joins two views; an owned fallback beside a view
is NK1185);
ADR-279 D5 (what ?? does to its left side);
ADR-282 D8 (.clone() is the copy, .to_owned() is NK1189);
ADR-282 D4, D5, D25 (where a literal is
built into text; a view kept needs a copy);
ADR-282 D12, D16 (text positions and
either_items);
ADR-184 D3 (ref Array[T], a run somebody else keeps);
ADR-182 D2 (a slice read carries no *);
ADR-233 D1, D4 (a sequence whose step pauses; a view of a value
that copies is read as the value);
ADR-285 D1 (no usize a program meets);
ADR-300 D10 (an abort reports the Nikaia line);
ADR-280 D5, D9 (what std throws, and
a named channel);
ADR-253 (a list has no +; extend joins two lists - the other
half of what a list offers, left a record of its own)
What does a container hand out, and what happens to what is handed in? Three questions that each answer part of the others:
m[k] - is it a T, aborting on an
absent key as Rust’s Index and Python’s __getitem__ do, or a T?? And
then: what is the read below, what does ?? join it with, what is a read of
a map whose values are themselves T?, and what is a key - a position, or
data the map keeps?collect(), what does collect() build, and what is a
slice’s type?rustc’s use of moved value about a file nobody wrote?Without one answer per question a first program meets a wall - an absent key
that ends the program, a named range or keys() that rustc will not walk, an
owned key that does not compile, a collect() into a map that writes a Vec,
a second use of a pushed value - and the checker passes what the language below
refuses (Part III C.1).
| Language / tool | Its answer | Source |
|---|---|---|
| Rust | m[k] aborts on an absent key; m[k] = v is refused altogether, so the asymmetry never arises. DoubleEndedIterator (from the back) and ExactSizeIterator (length known) are two traits, and take, zip, skip, step_by need both to reverse. Range<i64> has no ExactSizeIterator. Option<&Option<V>> from a map of options |
standard library signatures |
| Python | d[k] raises on an absent key; d.get(k) answers None |
from memory |
| Kotlin, TypeScript, Dart, Java | a map of V? reads one V?; containsKey / has tells absent from stored null. Java’s ConcurrentHashMap refuses null values, for a two-step race |
from memory, not checked |
| Scala, Haskell | nested: Option[Option[V]], Maybe (Maybe v), flattened by the program |
from memory |
| Swift | [K: V?] reads a V??, and dict[k] = nil removes the key |
from memory, not checked |
| Go, C# | one step answers both: v, ok := m[k], TryGetValue |
language specification |
What does not transfer. Rust refuses the bracket write; this language has
it (ADR-080 D2), so keeping Rust’s aborting read would leave the
most common absence the one spelling that crashes. A nullable here never nests
(Part I 2.3), which puts the language in Kotlin’s family for a map of T?.
Java’s refusal of null values is a concurrency reason that a door over a
shared map removes. And Range<i64>’s missing length is a fact about Rust’s
usize on a 32-bit target, which a range in this language must not inherit.
| Question | Rejected | Why it lost |
|---|---|---|
| a map read | keep the abort | absence is ordinary for a key that comes from data; the crash is the one place the program does not have to answer a T? |
?? over a view and a value |
copy the view out with Clone |
an allocation nobody wrote (ADR-283 D3) |
a map of T? |
refuse T? as a value type |
a second container for every program with an absent value, for Java’s concurrency reason, which doors remove |
a map of T? |
keep the two answers apart (T??) |
every read pays the rare question, and it is the nesting Part I 2.3 rules out |
| what a sequence can do backwards | one word, reversible | wrong either way: refuses chars().rev() (no length) or allows lines.filter(f).zip(ys).rev() (no length where one is needed) |
| reversing a pipeline | rev only on containers |
every reversal of a pipeline a collect() first - the heap copy the language exists to avoid |
| a stream narrowed from both ends | a cursor with next() / next_back() |
a new abstraction rather than a word on Seq; index arithmetic on views and a grammar already cover it. Left out by the owner until a program needs it |
collect() into a declared map |
refuse it (Seq::collect hands back Vec[$T]) |
a wall; the owner chose build what the place declares |
| a key in the brackets | wrap it in index::at, as a position |
index::at makes a number a usize and passes a view through: an owned key did not compile, an i64 key became a usize |
| a taking within one statement | order reads by statement only | keep(name, name) and s.count() + s.count() reached rustc |
m[k] reads a T?.let n = counts[fragment]?.n ?? 0
let user = users[request.query("name")] ?? return respond_404()
let counts = report.paths[path] ?? panic(f"{path} was a key a moment ago")
The bracket says what get says. ?. reaches through the result and ??
ends the chain. A program that knows the key is present says so on the right
of the ??, so an abort is one the program wrote.
m[k] = v inserts or replaces (ADR-080 D2). m[k] += 1 reads a
T?, so the form is m[k] = (m[k] ?? 0) + 1 - the fallback says what an
absent key counts as. A compound assignment to a map slot is refused with that
line in the message (NK1162).
xs[i] is a T, and an i outside the list ends the program (Part III A.2).
An index is arithmetic the program controls; a key is data it does not.
m[k] lowers to nikaia_std::index::get, one trait with an output type per
container - the element’s place for a sequence, an Option<&V> for a map - so
the emitter, which does not know types, writes the same call for both, and the
checker, which knows the container, types the expression. A position goes in
through index::at; a key into a map with owned keys goes in as D27 says. The
value reached is a copy where V copies and a view of the map otherwise.
?? joins a view with a value, and the language below decides which.A map read is an Option<&V>; the fallback is written as the value it stands
for. nikaia_std::index::or joins them by two impls that do not overlap - one
over Option<T>, one over Option<&T> where T copies - so a number is
copied out, a struct stays a view, and the emitter chooses nothing. Where the
value does not copy the fallback is a view too; an owned fallback beside a view
is NK1185 (ADR-278 D20), and a copy is written .clone()
(ADR-282 D8).
panic(message) exists, because it is D1’s own way out.It hands back never, so it stands where a value is wanted, and it reports
the Nikaia line through the hook ADR-300 D10 installs. It is on
Part I 1.3’s list and Part III A.2’s, and it is not assert.
T? is a nullable view.&str? is Part I 2.3’s own spelling: the checker types let s = &m[k] as a
nullable view, so s.min meets NK1125 and not rustc.
m[k] = (m[k] ?? 0) + 1 would otherwise put a &m inside the arguments of a
set(&mut m, …). A let below ends the read before the write begins.
* over the call, and a map’s answer derefs to the option.One trait answers a place for a sequence and an option for a map only if the
caller writes the same thing for each: a *. For a sequence it is the element
in its place - a sequence read cannot be a value below, since moving it out
would move out of the container; for a map it takes apart a small wrapper
(index::Found) whose Deref hands back the option. The * is written bare;
parentheses are added only under a postfix (D33).
a ?? b on a T? is a T, and the checker says so.let s = m[k] ?? panic(…) gives s the map’s value type, so s.mean() has a
receiver. A ?? over something the checker could not type still claims
nothing (Part III C.4).
io::Lines’ step throws io::IoError.Beside ADR-280 D5’s list: a step of io::lines yields
io::IoError, so a loop in a function with a named channel
(ADR-280 D9) has a named error on both sides of its ?.
T? is one T?.m[k] and m.get(k) on a map of T? are a T?. An absent key and a key
holding null both answer null; m.contains_key(k) tells them apart.
m[k] = null stores null; removing a key is remove.After m[k] = null the key is there, contains_key says so and len counts
it. The write and the read mean the same null - the trap Swift’s = nil
falls into.
Option<&T> below.The lowering is index::flat, and_then(Option::as_ref) over the read.
Nothing is copied, and the read is handed to a ref T? parameter as it is.
T? of its own is wanted, a map’s read is copied out if the value copies, and refused otherwise.For every map. An annotated let, an assignment, a return, a field or an
argument that keeps a T? of its own copies the read out where T copies
(ADR-233 D4); where it does not, NK1102 names m[k]?.clone().
Seq[T] say what the sequence is as a whole.Beside the step’s sync, pauses and throws: ends - it can be walked
from the back (a range, a list’s iter(), drain(), chars(), windows,
chunks); sized - its length is known before it is walked (the same,
and a map’s keys(), values(), drain(); not chars(), not a filter);
replays - walking it does not use it up (only a range). None is in a
program’s type grammar (ADR-105 D4): the ledger names them, the
checker reads them, and a message never says them.
Seq::rev writes its receiver Seq[$T] ends; a sequence without the word is
NK2703, in the program’s words, with the way out (collect() it and walk the
list from the back). A result has a word where its entry writes it and every
sequence handed in has it. The ledger column ends_by_length = true says a
result has a back end only where every input’s length is known - take,
zip, skip, step_by.
a..<b and a..b are Seq[T] sync ends sized replays, and NK2702 never
applies to one. A range kept, handed on or called on is
nikaia_std::range::Span (..<) or Through (..) below: Copy, its own
iterator, a length for every integer type. A range written straight into a
for or into brackets stays the language below’s a..b.
Every read of a name that holds a sequence takes it - a let, an argument,
a return, a field, every method - except an assignment’s target and a &;
NK2702 refuses the next use. A sequence from outside a loop taken inside it,
or taken inside a lambda, is refused where it is taken, unless the loop gives
the name a new one or may leave on that turn. Two arms of one if or match
are not one after the other. A for over a name that holds a sequence is
handed it rather than lent it. Where in its statement a read stands, and what
path it reads, are D31 and D32.
map keeps its element type: $U binds from what the lambda’s body comes to.
Seq::rev, zip, take, skip, step_by, and on a list iter, windows
and chunks are the language below’s own adapters, so a chain is one loop. A
count - take, skip, step_by, nth, chunks, windows - is an i64
here and a usize below, converted where the call resolved to that entry.
enumerate is not among them: its position is a usize
(ADR-285 D1).
HashMap::insert is the write under the language below’s name.(ref HashMap[$K, $V], key: $K, value: $V) -> $V?, keeping both. m[k] = v
stays the language’s form; insert is for a program that wants the value it
replaced. A text literal at a position the receiver binds to String is
built into text of its own.
str::chars says what String::chars says.Seq[char] sync ends.
ref Array[T], a run a function can take.A range in a list’s brackets reads a run of type ref Array[T]
(ADR-184 D3), &[T] below; a text slice is a view of text; a
list’s windows and chunks hand out ref Array[$T]. A slice fits a
parameter declared ref Array[T], as a list does; a ref Vec[T] parameter
refuses one in this language’s words. A range kept in a name slices exactly as
a written one, with no * (ADR-182 D2).
std’s is to_owned below..clone() of a &str or &[T] below clones the reference, so a clone the
checker resolved to a std entry (String::clone, str::clone,
Vec::clone) is written to_owned. A program’s own clone is untouched.
That .to_owned() is not written by a program is
ADR-282 D8’s.
collect() builds a list, or what the let, field or result it goes into declares.Where nothing declares anything, a Vec. Where a let, a struct field or a
function’s result declares a map the items fit as pairs, a set they fit, or
String from characters or pieces of text, that is what it builds, with no
list in between; below, the turbofish is left off. What the items do not fit
is NK1103. An argument is not such a place. Where the place declares C[T]?
and the items are T?, it builds the C[T] and is null at the first null
item, which ends the walk (#1161).
either_keys, either_values or either_pairs.As a list’s items go through either_items() (ADR-282 D16). To
decide it, the pass that decides text tiers reads what map makes as what its
lambda hands back; filter, rev, skip, take and step_by hand on what
they were given; a number, a truth value and null are no text of either kind.
For a map whose keys are owned, the checker tells the emitter per key what the
brackets do, and index::at is not written. Written, the map keeps the
key: a text literal is built into text where it stands (ADR-282
D2), a name is handed over (D29), a view is refused (NK1105) with
ADR-282 D25’s sentence. Read, the key is lent (&name,
&(k + 1)), or goes in as it is where it is a view already. A map keyed by
views is unchanged.
?? is a view.s = "b" and ids[k] = "seven" build the literal into a String, as a let,
a field, a return and an argument do. m[k] ?? "-" over a map of text is a
view of text (index::Or<&str> for Option<&String>); a place that keeps it as
a String is told so in ADR-282’s words.
NK2105.The places a value is given away: an argument the callee keeps, a key or value
written into a map or a list, a field of a struct being built (unless declared
a view), an element of a list or tuple literal, a let that names it again,
an assignment. A later read of what was taken is NK2105, with .clone() at
the hand-over as the way out; a number, a bool, a char and a view copy and
are not recorded. Loops, lambdas, the arms of one choice and an assignment
between are as for a sequence (D19).
A walk or a hand-over inside an arm of an if or match, or a catch
handler, whose last statement is a return or a throw is asked about reads
up to the end of its statement and no further - for NK2105 and NK2702
alike. A break or continue leaves a branch, not the function, and is not
this.
Every read has an order in the walk, which is the order the language below
evaluates: arguments, operands and struct fields left to right, a receiver
before its arguments. A read later in the same statement is after a taking:
keep(name, name) and s.count() + s.count() are refused. An assignment in
the same statement gives back what it took: name = name + "x" is a program.
A taking of p.name touches a later read of p.name, of anything inside it,
and of p; p.x is untouched. An assignment to p.name or p gives it back.
A part of a value only lent here - a ref parameter or self, a for
binding over a list, a let over a place - handed to something that keeps it
is NK2106, naming p.name.clone(). A part handed to what keeps it keeps its
parameter: fn f(p: P) { xs.push(p.name) } owns p; a field that copies keeps
nothing, one whose type the file cannot see keeps.
*index::get(…) is written bare; every postfix is written through one function
(postfix_base), which adds them where (*get(…)).len() needs them. So rustc
warns about nothing in a read.
?? panic(…) and says why.T? reads like a map of T, and present but null is one
contains_key beside the read - one step inside a door.Range costs on a clone(); a range in a for is unchanged. A
pipeline is reversed, zipped and cut without a list in between.NK2702 for a sequence and NK2105
/ NK2106 for a value, within a statement and per field; the way out is
named.collect() into the declared container are what a Rust programmer writes by
hand. The checker keeps a list of reads with an order and a path per body,
and the ledger three more words on a Seq..clone().The rules a program follows are Part I 3.3 (ranges and for), 4.5 (maps,
lists) and 6.5 (what is handed over), and Part III 13.5 (Seq’s words,
collect), A.2 and C.3.
entry-style door of the language’s own - the slot, made if
absent - which m[k] = (m[k] ?? 0) + 1 looks up twice. HashMap::entry,
or_insert, or_insert_with and and_modify are in std.contracts and
Part I teaches them, but they are std surface taken from Rust as-is
(ADR-288 D11), without a decision of their own.null, as Go’s
v, ok and C#’s TryGetValue do.collect() as an argument, and a VecDeque - std has no deque a
program can name.String, unchanged.T? by choice, as a get on a list
would be; std’s ledger has none.