ADR-293: A Map Read Is a 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 Part

Status: 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)

1. Question

What does a container hand out, and what happens to what is handed in? Three questions that each answer part of the others:

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).

2. How others do it

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.

3. Options, weighed

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

4. Decision

4.1 — A map read through the brackets

D1 — On a map, 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.

D2 — Writing is unchanged, and a compound assignment is written out.

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).

D3 — A list keeps its abort.

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.

D4 — The read is a view, and the emitter writes one call.

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.

D5 — ?? 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).

D6 — 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.

D7 — A view of a 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.

D8 — A write through the brackets binds its value first.

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.

D9 — A read is a * 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).

D10 — 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).

D11 — 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 ?.

D12 — A read of a map whose values are 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.

D13 — 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.

D14 — The flattened read is still a view, an 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.

D15 — Kept where a 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().

4.2 — A sequence

D16 — Three words after 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.

D17 — On a receiver the words are a demand; on a result they pass through.

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.

D18 — A range is a sequence that replays, and below it is a value.

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.

D19 — Every place a sequence is taken is seen, and a named one is walked by value.

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.

D20 — The pipeline, none of it allocating.

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).

D21 — 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.

D22 — str::chars says what String::chars says.

Seq[char] sync ends.

D23 — A slice is 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).

D24 — A copy of 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.

D25 — 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).

D26 — Into a map whose key or value is mixed text, the pairs go through 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.

4.3 — What is handed over

D27 — A key goes into a map as a key: handed over when written, lent when read.

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.

D28 — A literal kept by an assignment is built there; a map’s text read with a literal after ?? 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.

D29 — Data used after it was handed to something that keeps it is 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).

D30 — A branch that leaves the function takes its takings with it.

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.

D31 — A read has a place in its statement.

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.

D32 — A read has a path, so a part can be handed over and the rest kept.

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.

D33 — A read through the brackets carries no parentheses of its own.

*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.

5. Consequences

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.

6. What this does not decide

This site is open source. Improve this page.