String Where One Is Wanted — Constructed Where It Is Kept, Handed Over as It Is Where It Is ReadStatus: 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)
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.
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:
let, a return,
an argument the callee keeps — the literal is constructed there. That is
exactly what the program paid when it wrote .to_string(), and never more.&str below and the
literal goes in as it is: nothing is allocated at all.No new type, no new runtime, no cost on any value that is not a literal. This record takes C.
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.
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.
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").
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.
let s = "x" and then
Person { name: s } is still refused with the .to_owned() help: the
binding’s type is ref String, and making it follow a later use is inference
across statements, which is a larger question than this one. Written directly,
or annotated let s: String = "x", it works — and the refusal says exactly
that (ADR-208 D2).String is wanted,
because a method’s arguments are not lent yet (ADR-094, lends).
That costs what .to_string() cost; D3 reaches it when methods lend.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.
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.