ADR-207: A Text Literal Is a String Where One Is Wanted — Constructed Where It Is Kept, Handed Over as It Is Where It Is Read

Status: Superseded by ADR-282 — its D1-D4 are D3-D6 there Date: September 24, 2026 Target Version: Nikaia 0.0.183 Answers: the owner’s request to take down the wall a pre-alpha tester meets first — a literal where a String is declared is refused until .to_string() — with the instruction that goes with it: the complexity belongs in the compiler and not with the person writing the program, but clever rather than complicated, and not more expensive than it has to be Related: ADR-107 (text is one type; this builds the half of its D1 a program sees, by another road), ADR-094 (the keeps column and the lent parameter this reads), ADR-152 D4 (a literal whose lowering is decided by its use — the precedent), ADR-005 §3 (no clone the program did not write), ADR-008 (the tether, which is what ADR-107’s representation is still for)

1. Context

struct Person {
    name: String,
}

fn main() {
    let p = Person { name: "Ada" }
}

was NK1106 — Person.name is String, and this is ref String — with a help line that said make it a String and not how. The same program with a let s: String = "x", a return "x" out of a -> String, or greet("x") for a greet(who: String) was NK1103, NK1104 or NK1102. It is the first refusal anyone meets, and fourteen of the 22 .to_string() in examples/ were there to answer it.

ADR-107 D1 decided long ago that it should not be refused, and nothing of it was built, because its §5 routes the whole of it through a new representation of text: a String that is a view, a tether or an owned buffer, chosen per use, laid out as Bytes is.

So the question this record answers is not whether — that was decided — but which road is worth building.

2. The three roads, and what each costs

A — ADR-107’s representation. Text becomes a handle of three states. What it buys for a literal is a String field holding one without allocating. What it costs is paid by every text value in every program: one word wider than a view, a branch or a handle on every read and every drop, and a conversion at every edge to std’s Rust half and every described crate, which speak String and &str. And the state it exists to add — tethered — is the one ADR-008’s analysis found nothing in the corpus needs (#196, the tether). Paying everywhere for a saving in one place is the thing the owner said not to do.

B — insert .to_string() wherever a literal meets a String. Small, and wrong in the one place it matters: greet("x") for a greet that only reads its argument would allocate a String to point at. That is more expensive than the program needs, silently, which is ADR-005 §3’s objection in its proper form.

C — decide per position, from what the compiler already knows. The ledger already answers, for every parameter, whether the body keeps it or only reads it (ADR-094); the checker already answers, for every position, what type it wants. Those two answers are enough:

No new type, no new runtime, no cost on any value that is not a literal. This record takes C.

3. Decisions

D1 — A text literal is a String wherever the use asks for one.

An annotated let, a struct field, a return and a body’s last expression, an argument, a T? slot, a Vec[String] list literal — and through the arms of an if, a match and a block, where every arm ends in a literal. And beside text of its own: a list that already holds a String is a list of String, and an if or match whose other arms are String is a String, so ["a", f"c{n}"] and if c { name } else { "anonymous" } are the programs they read as. A literal has no other answer there — a list holds one type and a choice hands back one.

D2 — Where the text is kept, the literal is constructed where it stands.

String::from("Ada"), written at the literal, decided by the checker and handed to the emitter by the literal’s own byte (LitStr carries its position for this, as ListLit does for ADR-152 D4). It is the same rule that makes [1, 2] a vec![1, 2] where a Vec is wanted: a literal is a constant being built, not text the program had being copied.

ADR-005 §3 is untouched, because it is about the second: a view of text the program has — a parameter declared ref String, a slice of a file — put where text of its own is kept is still refused, and the help now says what to write: .to_owned(), a copy is written where it happens (ADR-107 D3). It used to say make it a String, because the arm that knew better matched a spelling (&str) the checker no longer prints.

D3 — A String parameter the body only reads is a &str below.

It was a &String. Every caller’s String reaches a &str through the & the call already writes, so nothing changes for them; a literal reaches it as it is, which is what makes greet("Ada") cost nothing. The & the call would write in front of a literal is left out, so the Rust reads greet("Ada") and not greet(&"Ada").

D4 — The keeps column sees every way a value leaves.

Found while building D1, and a defect in its own right — present before this record and independent of it: a parameter was kept only where it stood behind a written return or an assignment. So

fn same(name: String) -> String { name }

lowered to fn same(name: &String) -> String { name } and failed in rustc with mismatched types about a file nobody wrote — for the shortest program that keeps anything. The same for an if’s or a match’s arm and for let s = name then return s. The walk now reads the body’s last expression as a return, descends through if, match and a block to the value each one ends in, and follows a name a let bound to a parameter. A let whose second name is only read still keeps nothing.

4. What this does not decide

5. What it costs

A kept literal: one allocation of its length, at the site — what the program paid before by writing it out. A read literal: nothing. A String handed to a reading parameter: a &str is two words where a &String was one, which is one register per such argument, and the deref at the call is a load the callee would otherwise have done itself. No value that is not a literal changes shape.

6. What is built

All of it, in 0.0.183: the literal’s position, owned_texts from the checker to the emitter, the positions of D1 including the neighbours rule for lists and arms, D3’s parameter, D4’s walk, the .to_owned() help, the fourteen literal .to_string() gone from examples/ (the eight left copy a view, convert a foreign pointer, or are the foreign-runtime fixtures, which measure something else), and crates/nikaia/tests/text_literals.rs and keeps.rs holding each of them — compiled and run, not only lowered.

This site is open source. Improve this page.