ADR-156: Bytes Is the Language’s, and the Tether It Needs Is Refused Until It Is Built

Status: Superseded by ADR-283 — its D1-D3 are D19-D21 there, D6 is D22, D5’s polarity is D13’s; D4 is not carried (displaced by ADR-209) Date: September 20, 2026 Target Version: Nikaia 0.0.71 Related: ADR-154 D1 and §4 (the prelude’s list, and the door this closes), ADR-008 D2, D5, D9 (the three states and the residual hard error), ADR-140 (one spelling per thing), Part I 1.3, Part I 6.6, Part III 17.1, Part III 17.2, Part III C.1, Part III C.4

1. Context

ADR-154 D1 puts Bytes on Part I 1.3’s list, beside Vec and String, as the containers a program cannot do without. §4 of that record left open whether it is std’s or the language’s — it is in the list because a program cannot do without it, wherever it lives. Enforcing the list (§5) is what turned that door into a question: what needs no use is what std’s ledger keys bare, so the two answers stopped being interchangeable. One of them puts Bytes in a module and behind a use, which is the opposite of being on the list.

And the name reached nothing. Bytes did not exist — not in std, not in the compiler’s built-in names, not in any .nika file. A name on a prelude’s list that does not exist is the one direction a prelude can be wrong in without anybody noticing, because nothing refuses it: nothing gets that far.

It was never a missing name, either. Part III 17.2 already says what it is: read returns Bytes, not a Vec[u8]: it is one shared buffer, and slices that outlive its scope are tethered to it (Part I 6.6). So Bytes is the container the tether needs — and the tether is the half of Part I 6.6 that is not built. ADR-008 D2’s three states are Borrowed ⊑ Tethered ⊑ Owned; the first is the language below’s own lifetime, the third is .to_owned() written by the program, and the middle one is a reference-counted buffer that does not exist. What exists in its place is D5’s residual hard error.

That is the shape of the question this answers: a type whose surface is specified, whose mechanism is not, and whose absence from the tree is the only reason nothing is broken.

2. Decisions

D1 — Bytes is a language type, written bare.

It is a name this compiler knows, like Vec, String and Array, and it stays on Part I 1.3’s list. Not std’s: a type whose representation the compiler picks — which is what Part I 6.6’s states are — is not a type a module owns, and putting it behind a use would make fs::read’s result a name a program must import before it can write down what it already holds.

D2 — It is one shared buffer.

A Bytes is a reference-counted, immutable run of bytes: handing one on costs a count, never a copy. It reads as a run of bytes everywhere a &[u8] is taken, so nothing written against the language below has to know what it is. Building bytes is a Vec[u8] handed over, which is the one copy in the type and the program’s to write.

D3 — fs::read hands one back.

Which is what Part III 17.1 and 17.2 have said all along and the ledger did not: it keyed Vec[u8]. Reading a file and passing it on now costs a reference count rather than a copy of the file.

D4 — A view of a buffer the body owns is refused — NK2303.

This is the mechanism Bytes hangs on, and it is not built, so the compiler says so in the one place a program can act on: the Nikaia line. Where a function hands back a view that points into a buffer its own body made, the program is refused, with the buffer named and the two ways out — take the buffer as a parameter, or hand back a copy with .to_owned(). The compiler never inserts that copy (ADR-008 D5).

D5 — The refusal stands on a named buffer and never on a doubt.

The analysis errs towards Tethered, which is ADR-008 D7’s own polarity: a state it cannot decide is the wider one, and being wrong costs a representation wider than it had to be. A refusal may not err that way. Refusing a correct program is the worse of the two mistakes (Part III C.4), so NK2303 fires only where the walk can name the buffer’s type and the returned expression is derived from the local that holds it. A call no ledger describes raises nothing.

D6 — Mapped does not deref to Bytes yet.

Part III 17.2 says it does, and that sentence stays a promise rather than becoming a lie in code: fs::map hands back a mapping whose deref is &str today, which is what examples/1brc.nika parses. Changing it is the tether’s own work, where a mapping and a read really do become the same thing to a parser.

3. Consequences

Part I 1.3’s list is true for one more name. Two are left — assert and panic — and both belong with Part III 14, which is not built. The Status note on that section says so and now says it about two names instead of three.

docs/open-decisions.md loses its Bytes entry, which is what an answer does to a question: the entry leaves rather than staying with a note on it. The page recommended C now, B when the tether is built; this record is B, built now, with D4 in the place of the mechanism. The reason is the cost the page itself named for C: a program written between now and the tether would write Vec[u8] and get a second spelling for one thing when Bytes arrived, which is the problem ADR-140 spent a whole record removing.

A shape rustc used to speak about now has a Nikaia message. A function that hands back a view of a local buffer lowered to Rust that does not compile, and the diagnostic named a type in a generated file. Part III C.1 says rustc must never speak about that file; D4 is where that stops for this shape.

Nothing in the tree is refused by it. Every view in every signature of examples/ and benches/ solves to Borrowed — ADR-008 §3’s free case, measured — and the_corpus_needs_no_tether in crates/nikaia/tests/bytes.rs is that claim held to, over every .nika file in the repository.

4. What this does not decide

5. What is built

This site is open source. Improve this page.