ADR-114: Reading a Map Through the Brackets Is a T?, Because a Key Is Data

Status: Superseded by ADR-293 — its D1-D4 are D1-D4 there Date: September 16, 2026 Target Version: Nikaia 0.0.8 Related: ADR-080 D2 (a write through the brackets is an insert), ADR-052 (T? is a type, and ?. reaches through one), ADR-066 D4 (?? chains), ADR-113 (a reach is through a view), ADR-048 D1 (an index is an i64), ADR-011 D2 (the emitter does not know types), Part I 2.3 and 4.5, Part III Appendix A

1. Context

One pair of brackets on a map did three things. scores["Player1"] = 100 inserted. scores.get("Player1") answered a T?. And scores["Player1"] read aborted the program where the key was absent, as Rust’s Index and Python’s __getitem__ do.

The third is the one that does not belong in this language. T? exists so that absence is a fact in the type and the compiler makes the program say what happens then, instead of the program finding out from a customer. A map is where absence is ordinary: a key comes from a request, a file, a person, and whether it is present is a property of the data and not of the program. The one place absence is most common was the one place a spelling went around the type and crashed. Rust avoids the asymmetry by refusing m[k] = v altogether; this language had added the write and kept the abort.

A list is different, and stays different. xs[i] with an i outside the list is a mistake in the program — a wrong loop, a wrong sum — and Index Out of Bounds stands in Appendix A among the states that are a bug rather than a condition of the world. An index is arithmetic the program controls; a key is data it does not.

2. Decisions

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, and get stays, saying the same. ?. reaches through the result and ?? ends the chain, as Part I 3.5 has them. A program that knows the key is present says so on the right of the ??, and the abort it used to get for free is the one it now writes.

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

m[k] = v inserts or replaces (ADR-080 D2). m[k] += 1 reads the slot and writes it, and the read is a T?, so the form is m[k] = (m[k] ?? 0) + 1 — the fallback says what an absent key counts as, which is the question ADR-080 left open and this answers. A += on a map slot is refused with that line in the message.

D3 — A list keeps its abort.

xs[i] is a T, and an i outside the list ends the program as Appendix A says. The line between the two containers is the line between arithmetic and data, and it is drawn once.

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

m[k] lowers to nikaia_std::index::get(&m, index::at(k)), one trait with an output type per container — a T for a sequence, an Option<&V> for a map — so that the emitter, which does not know types, writes the same three tokens for both and the language below picks (ADR-080 D2’s arrangement, read side). The checker, which does know the container, types the expression. The value reached is a copy where V copies and a view of the map otherwise (ADR-113 D2).

3. Consequences

4. What this does not decide

5. What is built

Nothing. A map read through the brackets lowers to Rust’s Index and panics on an absent key; += on a map slot goes through the same Index. The order of work:

  1. nikaia_std::index::get with an output type per container;
  2. the checker typing a map’s bracket read as T?, and the += refusal;
  3. the two example lines, Part I 4.5’s example, and a test that reads an absent key and gets null.

#261 carries it.

This site is open source. Improve this page.