String, Whose Representation Is Decided per Position by What Flows In — and a Copy Is Only the .clone() the Program WritesStatus: 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
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.
| 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.
| 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 |
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.
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.
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" }.
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).
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.
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.
.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.
.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.
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.
.clone().NK1105, NK1106, NK1183, NK2302, NK2304, the tether’s refusals, the
specification and the README.
Views where a String is wanted.
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.
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.
| 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
ref String: the help leads with declare it
String, which copies nothing, and gives .clone() second;String wherever it is
kept as text of its own, and kept as a backstop: it costs one branch, and
a binding D17 ever leaves a view would get the right sentence rather than
the general one;.clone().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).
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.
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.
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.
.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).
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).
String is wanted; a view stands in a field, a result, a parameter, a let,
a list, a set, a map and a String?; and what is refused is the published
case (D22, D24) and a foreign crate that keeps (D27), each with a reason and a
paste-ready way out.String below,
and compiles to the Rust it would without this record..to_string() cost. A literal read, or kept
where only views go: nothing (D29). A String handed to
a reading parameter: a &str of two words where a &String was one. A mixed
position: one branch on a read, and nothing else anywhere..to_string() in examples/
existed only to satisfy a type (D3); 81 test programs wrote
"…".to_string(), which is what kept .to_string() from being refused on
text (D8)..clone(), and
the one for a ref String parameter leads with the answer that copies nothing.NK1261; Part I 2.3 states D3, D4 and
D7-D8; Part III C.4’s example is a published field (D22, D25)..to_string() on text should be hinted or refused now that it is
redundant (D8): the owner’s to decide.lends), so D5 and D11 do not reach obj.show(city)
and a literal argument to a method is constructed; both follow when methods
lend.Vec view kept where a Vec
is wanted; it is not decided here.ref T for other types outside the C boundary (ref Array[T],
ref Vec[T], ref on a struct). D2 is decided for text; whether the same
direction holds for every non-scalar ref T is open in
#628: unlike text,
ref there also selects a view where the declared type alone would be a
value of its own (ADR-311’s ref Array[T]), so it is not
redundant by the same argument. A word-sized scalar is decided:
ADR-views-3 D4.char sequence. chars() is a Seq[char]
(ADR-105), unchanged.collect. Not a tier question: collect builds a
list, and the checker accepting it into a declared map is
#208.