ADR-282: Text Is One Type, String, Whose Representation Is Decided per Position by What Flows In — and a Copy Is Only the .clone() the Program Writes

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: is a literal, or a view of text, refused where a String is declared until .to_string() copies it? (#200, a view kept where a String is declared: a copy or a tether?, and the README wall that came with it); does .to_owned() still belong to the language?; #260 (a view pushed into a list a published function handed back reaches rustc); and the owner’s rules the answers were held to: the complexity belongs in the compiler, but clever rather than complicated, the costs are carried by the special case, not by the general one, and Nikaia does not build walls, it takes them down; #628 (what is ref String still needed for?); #659 (is a position only literals fill a view of static text, or text of its own?) Related: ADR-005 §3 and ADR-283 D3 — no copy the program did not write; ADR-283 — the states a view is in, and where its buffer lives; ADR-094 — keeps, and the & a declaration writes and a call does not; ADR-184 D1-D2 — a view is spelled ref X, and text has one noun; ADR-229 — a value handed over inside an f"…" hole; ADR-293 D24 — the compiler writes to_owned below for a clone; ADR-290 — a described crate’s entries; ADR-037 D8 — a published representation is chosen before its users exist; ADR-278 — the nullable wrap; ADR-152 D4 — a literal whose lowering its use decides; ADR-300 — a rustc message mapped to a Nikaia line

1. Question

Where a program declares String, what does it get below — a view, text of its own, or either — and who decides: the program, by writing a second type and a copy, or the compiler, by what flows into that place?

Without an answer the first thing a newcomer meets is a refusal: a literal, or a view of text the program already has, put where a String is declared, and a help line that says write .to_string() — a copy the program did not need. Fourteen of the 22 .to_string() in examples/ existed only for that. And a place the checker lets through and rustc then refuses — a list of views declared Vec[String], a map of word counts keyed by views, a view in a String? — is Part III C.1’s one forbidden outcome: an error about a file nobody wrote.

The question cannot default, because every default costs someone. A view everywhere makes every line with text of its own find a home for it; text of its own everywhere is a copy nobody wrote; a refusal is the wall.

2. How others do it

Language / tool Its answer What it costs there See
Rust two types, String owns and &str views; the program picks, in the type. Two words for a copy: .clone() of a reference copies the reference, so &str and &[T] need .to_owned() every signature chooses; a literal where a String is wanted needs .to_string() —
Rust’s Cow<str> one value that is either borrowed or owned, decided per value one branch on a read; as large as a String D14, which builds EitherText over it
Nikaia’s own Bytes a buffer in one of three states — borrowed, tethered, owned — that the compiler picks and the program cannot see one word wider than a bare view ADR-283 D19–D21
Nikaia’s sync, @borrowed, a &T parameter the compiler decides, and you may pin it: inferred, and an assertion written is held to none beyond the check ADR-288, ADR-094 D2

What does not transfer: Rust’s second noun exists because Rust has a reference to copy, and the program writes the reference; here the compiler writes it (ADR-094 D1), so the distinction is the compiler’s to keep. String keeps its name because every neighbouring language has it.

3. Options, weighed

Option What it is Why it won or lost See
A — a new representation of text every String a handle of three states, as Bytes is lost for literals and for the ordinary view: every text value in every program pays one word and a branch, plus a conversion at every edge to std and every described crate, for a saving in one place; and the state it adds, tethered, is one nothing in the corpus needed —
B — insert .to_string() where a literal meets a String the compiler writes the copy lost: greet("x") for a greet that only reads would allocate silently (ADR-005 §3) —
C — decide per position, from what the compiler already knows keeps says whether a parameter is kept; the checker knows what each position wants; a kept literal is built there, a read one is passed as it is won for literals (D3-D5) D3-D5
D — a view kept where String is declared: copy it the compiler inserts .clone() lost: a copy inserted runs on every execution of the line with nothing in the source to show it D7
E — make such a field a view everywhere one representation, the view lost: every line with text of its own must then find a home for it; the general case pays for the special one —
F — per position, by what flows in: own, view, or either per value three tiers, each paid only where it is used won (D12-D14) D12
G — convert a list going into a mixed list, at the let a converted copy of the list lost: an allocation on every execution of the line, this record’s D7’s copy one level up; linking the two positions instead converts nothing (D16) D16
H — three words for a copy (.clone(), .to_owned(), .to_string()) as Rust has them lost: two sets of advice for one act, and one spelling per meaning is Part I’s rule; .to_string() refused on text was also rejected, since one expression would be right or wrong by its receiver’s type D8
I — refuse a published parameter both kinds flow into the ledger’s String holds for the package’s own calls too narrowed: the reason is about callers elsewhere, who only hand over text of their own; the package’s own calls were paying for a promise to someone else (D23) D23

4. Decision

One type.

D1 — Text is one type, String, and what lies below it is the compiler’s, per position.

A String value is a view of text that is already there, or text of its own, and the program never names which. The compiler picks the cheapest that works, by position: for a literal by what the use does with it (D3-D5), for every place a program declares String by what flows into it (D12-D13). A view of text is written ref String (ADR-184 D1-D2); str is the compiler’s word below and reaches no program, signature, ledger or message.

D2 — A String is a view wherever a view suffices, so ref String adds nothing inside Nikaia: the ledger states the public interface.

Nothing is copied that the program did not write (D7), so no copy is not a promise a signature has to make. What a published signature can do is read off the body and recorded in the ledger, as it is for every other type: whether a parameter is lent or kept (ADR-094), whether a field or a result is a view and which parameter it points into (ADR-views-3, D5, D7). A change to any of it is a ledger diff in review.

Inside Nikaia, ref String written on a parameter, a field, a result or a let is a warning, NK1261 (ref is redundant here, the ledger records it), and the compiler still holds the program to it (D25). It becomes a refusal once the specification, examples/ and tools/*.nika carry none.

Rejected: keeping ref String as the spelling of a view at pub. It is a second source for what the body already decides, written by hand, and the two can disagree; for no other type does a published signature carry it. Rejected too: refusing it at once, which would break every program written to the old rule in one release.

Literals.

D3 — 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, a tuple’s part, a ?? inside an f"…" hole, 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 arm beside a String arm is a String — ["a", f"c{n}"], if c { name } else { "anonymous" }.

D4 — Where text of its own 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 position — the 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, so ADR-005 §3 is untouched. Only where text of its own also flows into the position: one only literals and other views flow into holds the view (D29).

D5 — A String parameter the callee only reads is a &str below, whatever flows into it.

Every caller’s text reaches it through the & the call already writes; a literal reaches it as it is, with no & in front, so greet("Ada") allocates nothing. Its tier (D13) decides only what a keeping parameter is.

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

A parameter is kept where it leaves through a written return, an assignment, the body’s last expression, an if’s or a match’s arm or a block’s value, or a name a let bound to it. fn same(name: String) -> String { name } keeps name. A let whose second name is only read still keeps nothing.

Copies.

D7 — A copy is written or refused, never inserted, and its one word is .clone().

ADR-005 §3 stands: a copy of text costs as much as the text is long and runs every time its line does, so it is the program’s to write. The word is .clone(), for text and for everything else — a copy of text is text of its own. .to_owned() is NK1189, naming .clone(); a program’s own method called to_owned is not touched.

D8 — .to_string() is the text form of a value, and the text form of text is the text itself.

It stays, for every type, and is not a copy. On text it answers the receiver’s own type — a view stays a view, a literal a literal, owned text the same text handed over — and nothing is written below. A literal needs no .to_string() anywhere (D3). The keep and tether analyses read to_string of a literal as no buffer, of a name written with a non-text type as one, and of anything else as unsure, which no refusal stands on.

D9 — The compiler writes whichever copy the language below needs.

A clone that goes to a std entry, or of a slice (ref Array[T] into Vec[T]), is to_owned below. contracts::keep and contracts::tether read a clone of text — a literal, or a name whose type was written as text — as owning a buffer; a clone of anything else keeps pointing where the original’s views pointed.

D10 — Every help says .clone().

NK1105, NK1106, NK1183, NK2302, NK2304, the tether’s refusals, the specification and the README.

Views where a String is wanted.

D11 — A view handed to a String the callee only reads is lent as it is.

Where keeps says the callee reads the parameter, and the callee is a function this compiler declares, a view goes through unchanged: show(city) in the source and in the Rust. A ref written at such a call is still NK1137. A function of std or of a described crate is not included: its signature below is a description, and String there may mean a Rust String by value.

D12 — Every declared String and String? is a position.

A struct’s field; a function’s result; a function’s parameter, by every argument any call of the package passes; an annotated let, by its initialiser and every assignment; and, inside any of these, the element of a list or a set and the key and the value of a map, by what push, insert, m[k] = v and an initialiser put there. A String? is a position like a String, and null puts nothing into the decision. A type parameter’s String is not a position.

D13 — Each position is one of three tiers, by what flows in, and a value’s kind is read off what it is.

what flows in below who pays
text of its own only String, unchanged nobody
views only (literals count as views of static text) ref String, as if written; its buffer placed by ADR-283 and ADR-283 only what a view already costs there
both EitherText (D14) that position alone: one branch on a read

A literal beside text of its own moves nothing: a position only literals and text of its own flow into stays String, and the literal is built where it is kept (D4). A position only literals flow into is views only, and holds a view of static text (D29). A value’s kind: a literal is a view of static text; f"…", a .clone() and a String handed over are text of their own; a ref String parameter, a slice and what a method of text cuts from its receiver are views; a function of the program answers with its declared or decided result; a read of an element hands on what the position is. Anything the walk cannot read is text of its own, which leaves the old refusal and never a wrong program. The walk runs to a fixpoint over all positions, before anything else looks at the program, and writes its answer into the declared type, so every later reader sees one answer.

D14 — Both kinds are EitherText, and one conversion hands a value in.

nikaia_std::either_text::EitherText, over Cow<str>, as large as a String: a view is borrowed and text of its own is moved in, each at its own line; a read derefs to str, prints, compares and hashes as its text; its to_owned is what .clone() writes below. A value goes in through IntoEither (IntoEitherMaybe into a String?): the tier pass writes value.into_either(), the emitter writes either(value) where it wraps a field, a result or an argument, and the nullable wrap (ADR-278) goes around the conversion — Some(either(v)). A call inside an f"…" hole is wrapped like any other. Nothing is copied.

D15 — A mixed position reads as text of its own.

The checker reads an EitherText position, and a value handed over by into_either() or either_items(), as String: it is lent as a &str, .clone() copies it, a method of text reads through it, and it accepts a view as well as text of its own. A view handed to a parameter both kinds flow into is let through; the emitter hands it over.

D16 — A list going in whole goes item by item, or is linked to where it goes.

A list literal hands each item over as D14 says. A collect into a list whose element is mixed hands each item over as it is (either_items()), and the list is built once. A list that already has a position — a result, a field, a declared name — is linked to where it goes: one representation below, one tier from what flows into either, nothing converted at the let. A name bound to a list without a type is that list.

D17 — A name bound to a literal and kept as text of its own is declared String.

Where let n = "ada" is kept in a position that stays text of its own, the binding is declared String and the literal is built once, where it is bound. Where the position is a view, the name stays a view of static text.

An if or a match whose every value is a literal is one too (#441): let q = if c { "x" } else { "y" } is static text whichever way it goes, so kept as text of its own it is declared String, each literal built where it stands. And the rule needs no String written elsewhere in the program: a main that pushes let n = "ada" into a Vec[String] is declared the same as one beside a String field.

The pass reaches the package’s own types (#956, owner, 2026-10-29): a name kept in a String part of a tuple, put into or looked up in a container (set.remove((1, other)), Rust compares the whole element), and a name kept in a published String field of a published struct another file of the package declares, which is text of its own (D25), are declared String the same; the package’s loader decides each file’s tiers once all its files are parsed, reading those fields as written.

D18 — A view cut from text is typed as one, and text of its own has a view’s methods.

trim, trim_start, trim_end (-> ref String), lines, split, split_whitespace (-> Seq[ref String]) are described in std.contracts, each borrowing from its receiver. A method a String receiver has no entry for is looked up on str.

D19 — A kept argument is measured against what the receiver binds.

Where a signature writes $T for an argument the callee keeps (Vec::push(value: $T)) and the receiver binds $T to String, a view there is D25’s question: NK1102, naming .clone(). A literal is built where it is kept. What this reaches is only what the tier pass could not make a view or mixed.

D20 — A view handed back out of the one parameter that holds views is the buffer’s.

fn first(xs: Vec[ref String]) -> ref String hands back a view inside xs, so the buffer’s lifetime is named on the parameter and the result: fn first<'a>(xs: &Vec<&'a str>) -> &'a str, and a struct of views the same. Only where that parameter is lent, is all the function can borrow from, and is not a view itself; with two, which buffer it is is ADR-283’s question.

D21 — A mixed field handed back takes its struct, as a String field does.

EitherText may own its text, so it cannot leave a loan without a copy. So return e.key takes e: fn label(e: Entry<'_>) -> EitherText<'_>, and the keep inference reads a result both kinds flow into as one that hands a value over.

Published positions.

D22 — A published field or result is never both kinds.

A pub field of a pub struct and a pub function’s result leave with the package before their users exist (ADR-037 D8). Where the package puts only views there, the ledger says ref String (or ref String?); where it puts both, the view is refused (D25), because a mixed representation would need a type no other package can build.

D23 — A published parameter takes either kind of text from its own package.

Where the package passes only views, the parameter is ref String in the ledger and a caller elsewhere lends to it. Where it passes both, the ledger still says String, so another package hands over text of its own; below, the parameter is impl IntoEither<Either = EitherText<'e>> (IntoEitherMaybe for a String?), taken on the body’s first line. Only the parameter itself: a list of text it takes stays what it is published as.

D24 — Unless its value leaves through something published as text of its own.

pub fn greet(who: String) -> String { return who } hands the parameter back through a published result, which D22 keeps one kind. Such a parameter stays text of its own and a view handed to it is refused, naming .clone(). The tier pass follows a parameter’s value into the positions it goes into to decide it.

Refusal, report, boundary.

D25 — Where a view is kept and no position can take it, the refusal says why, for the case it is.

What is still refused: a view into a published position both kinds flow into (D22, D24), a view into a String? that stays text of its own (asked through the ?), and a view a kept argument’s receiver binds to text of its own (D19). The message says whose text it is, what keeps it, and what a copy made on its own would cost — ADR-005 §3’s reason — in three cases:

D26 — What the compiler chose is reported, not guessed.

nikaia --tethers lists every String position that is not text of its own below, and why, under text (declared String); the lines below it say where each view’s buffer lives (ADR-283). A hot loop that must not allocate reads the report; nothing in it allocates that the program did not write (D7).

D27 — At a foreign boundary the described signature decides, and a copy there is the program’s.

A described crate’s entry (ADR-290) says ref String or String in the ledger’s language. A Nikaia String of either kind crosses to ref String for free. Crossing to String — text the crate may keep — is NK1102, which says the crate keeps its parameter and names .clone(). A described position is the crate’s, so it gets no tier.

D28 — At the C boundary a view is written, required, and the body is held to it.

In an extern "C" block and on a pub extern fn (ADR-302 D5, ADR-284) a view is written ref String (or ref Array[u8], ref mut T), and String there means text handed over. C reads no ledger: a signature derived from the body could change while C callers still compile, and the program would then have undefined behaviour. So the written signature is the contract, and the compiler checks the body against it — a ref String parameter of a pub extern fn kept past the call is refused (D25) — rather than deriving the signature from the body. NK1261 does not apply there.

Literals kept.

D29 — A position only literals flow into holds a view of static text.

A field, a result, a let, a list’s element or a map’s key or value into which only literals flow - and, with them, other views - is views only (D13): it holds a view of the literal, &'static str below. Static text outlives every name, so the position needs no lifetime parameter and ties nothing to a buffer. As soon as text of its own also flows in, the position is what D13 says for both kinds; a published one stays one kind (D22), so it becomes text of its own and the literals are constructed (D4).

A literal is a view of the program’s own text (Part I 2.2); D3-D4 made that true where it is read and not where it is kept, so storing "Hamburg" in a field allocated a copy of bytes the binary already holds - about 437 instructions per stored literal on benches/template.nika (n rows, two literal fields; 41.0M against 23.6M instructions at 20 000 rows, callgrind, #659). Go, Java, Swift, Python and JavaScript store a literal without copying it; a script-like language whose literal allocates once it is stored is the exception. Once ref String is refused (D2, NK1261), a program has no way left to ask for the view, so the default must be the one that copies nothing.

Rejected: keeping D3-D4 as they were (every kept literal constructed). It charges the general case - a table of constant labels, a list of keywords - for a special one, against the owner’s rule that the special case carries the cost. Rejected too: a published position only literals flow into as mixed (EitherText, so a caller elsewhere may also store its own text). It is a second rule, and a branch on every read of every such field, for a caller that may not exist; a published view is already the general rule (D22), the ledger shows it in review, and a package that wants callers to store their own text puts some there itself. It can still be added later without changing any unpublished position.

D30 — A text literal takes no .to_string() and no .clone().

"-p".to_string() and "-p".clone() on a plain text literal are refused (NK1301); the help drops the call. A literal already fills any String slot (D1, Part I 2.2), and by D8 .to_string() of a view is that view, so both calls mean exactly the bare literal: a second spelling for the same thing, the case NK1189 settles for .to_owned(). x.to_string() on a value that is not a literal (a T, a number, a view held in a local) keeps its meaning.

Rejected: a warning only (it leaves two spellings in the code that ships and in what rs2nika writes); leaving it (the Rust habit then spreads through every port).

D31 — A literal where text of its own is needed is owned; the compiler decides.

A literal in a slot declared String is text of its own wherever the program needs it so: the binding is changed (let mut y: String = "hi", then y.push_str(..)), or it reaches a mut or owning parameter, also through a match binding (match s { t => shout(t) }) or as a lambda’s last expression beside a String branch. The program writes neither .clone() (refused by D30) nor anything else; the compiler makes the copy where the literal is constructed.

Reason: Nikaia reads like a scripting language; the programmer does not think about where text lives, and that complexity moves into the compiler (ADR-282’s premise). NK1105’s help already writes this spelling.

Rejected: f"hi" as the spelling of an owned literal (a second meaning for a template without holes, and memory management in the program’s text); "hi".clone() (D30).

5. Consequences

6. What this does not decide

This site is open source. Improve this page.