Open and answer this issue on GitHub.
ready · opened 2026-10-18 · updated 2026-10-19 · 3 comments
A text literal like "Hamburg" is part of the compiled program: the bytes already sit in the binary, for as long as the program runs. Where a program only reads a literal (println("Hamburg"), greet("Ada")), nothing is allocated. The question is what happens where a literal is stored — in a struct field, as a function’s result, in a list — and nothing but literals is ever stored there.
pub struct Row {
name: String,
note: String,
}
fn main() {
let mut rows = Vec()
for i in 0..<n {
rows.push(Row { name: "Hamburg", note: "a note with <angles>" })
}
...
}
Today every Row gets two fresh heap copies of text the binary already holds:
pub struct Row { name: String, note: String }
rows.push(Row { name: String::from("Hamburg"), note: String::from("a note with <angles>") });
With ref String written on the fields (as benches/template.nika does today), the same program lowers to views of the static text and copies nothing:
pub struct Row<'a, 'a1> { name: &'a str, note: &'a1 str }
rows.push(Row { name: "Hamburg", note: "a note with <angles>" });
#654 (ADR-282 D2: String is a view wherever a view suffices, so ref String is to go away) needs every ref String in our programs to be droppable without changing the lowering. The #654 builder (0.0.1022) found that this one position does change: benches/template.nika’s Row gets copies when ref is dropped. It is the only remaining class besides option defaults (#654 step 0 c1, being built). Nothing breaks either way — it is a cost question, and a question of what the language promises about literals. It needs an answer before NK1261 turns ref String into a refusal (#654 step 6), because after that a program has no way to ask for the view.
let, a return, an argument the callee keeps — it is constructed there, as [1, 2] is a Vec where one is wanted”. ADR-282 D3/D4 say the same, and D17 for a let and an all-literal if/match.String” — which covers literals mixed with text of its own, not literals alone. Whichever way this is decided, D13 is to be reworded.| what is stored | lowered today |
|---|---|
| a field filled only by literals | String::from("…") |
a result if big { "large" } else { "small" } |
String::from in each branch |
Team { names: ["Ada", "Grace"] } into Vec[String] |
vec[String::from(…), …] |
a local Vec filled by push("Ada"), then put in a Vec[String] field |
refused, NK1106 — contradicts D16 (“a list that already has a position is linked to where it goes”); a defect whichever option is chosen |
benches/template.nika (n rows through dsl html), both variants lowered by today’s compiler, rustc -C opt-level=3, no LTO, against the release nikaia_std; same output byte for byte. callgrind --branch-sim=yes:
| rows | view (&str) |
copy (String) |
copy / view |
|---|---|---|---|
| 2 000 | 2 891 572 instr., 30 642 mispredicts | 4 589 236 instr., 37 256 mispredicts | 1.59× |
| 20 000 | 23 550 344 instr., 228 721 mispredicts | 41 035 481 instr., 289 312 mispredicts | 1.74× |
That is ≈ 874 instructions per row, ≈ 437 per stored literal (an allocation, a copy, a free) — in a benchmark that also escapes and renders HTML for every row. Overflow checks on in both.
Go, Java, Swift, Python and JavaScript store a literal without copying it: the string value points at the static bytes (Java and Python intern them, Swift marks them immortal, Go’s string header points into read-only data). Rust makes the programmer choose (&'static str, String, or Cow<'static, str>). C++’s std::string copies (small strings inline), std::string_view does not. A script-like language whose literal costs an allocation the moment it is stored is the exception.
A — keep it: a stored literal is text of its own (Part I 2.3 as it stands).
Every literal kept in a field, result or list is allocated, once per evaluation. Nothing changes in the specification; D13’s sentence is reworded to say so. benches/template and every similar program pay ≈ 437 instructions per stored literal, and after NK1261 there is no way to avoid it.
B — a position only literals flow into is a view of static text (D13’s table as written).
Below it is &'static str — no lifetime parameter, since static text outlives everything. As soon as text of its own also flows in, the position is mixed (EitherText, D14) or owned, as today. Part I 2.3’s “constructed there” applies only where text of its own also flows in; D3, D4, D13, D17 are reworded. Risk: for a published struct, field or result, the representation depends on what the package itself puts there. A library whose pub struct is filled only with literals publishes a view of static text; a caller outside the package that wants to put its own f"…" there is refused (NK1106) until the library changes, which is a ledger diff (ADR-282 D2) — the same as for every other view today, but a new kind of surprise for a literal.
C — B inside a package, EitherText at a published position.
Unpublished positions as in B. A published position only literals flow into is mixed (Cow<'static, str> below): no allocation, any caller may store its own text, one branch per read. Costs a second rule and a branch on every read of such a field.
In every option, the NK1106 on a pushed-to local list (table above) is a defect and is fixed.
B. It makes “a literal is a view of static text” (Part I 2.5, 2.4’s example) true wherever the literal goes instead of only where it is read, it is what D13’s table already says, it removes ≈ 437 instructions per stored literal, and it is what every comparable language does. The published-position risk is the general rule for views (ADR-282 D2, NK1106), and the ledger shows the change in review; if that proves to bite real libraries, C can be added later without changing any unpublished position.
&'static str (no lifetime parameter). Test: benches/template.nika with ref dropped lowers byte-identical except &'static str for the generic lifetimes; the all-literal result and the list literal above lower without String::from.Vec above compiles. Test: that probe, and the same with one f"…" pushed (mixed).tools/*.nika regenerated; each change looked at.crates/nikaia/tests/measure.rs gets the A/B above as a measurement.Decided by the owner: option B. A literal is a view of the program’s own text wherever it goes, not only where it is read.
&'static str (no lifetime parameter): a field, a result (also an all-literal if/match), a let, a list element, a map key or value. Test: benches/template.nika with ref dropped from Row lowers without String::from and with &'static str fields; fn label(big: bool) -> String { if big { "large" } else { "small" } } and Team { names: ["Ada", "Grace"] } lower without String::from; with one f"…" beside the literals, the literals are constructed as today.let mut names = Vec(); names.push("Ada"); Team { names: names } compiles (no NK1106). Test: that probe; the same with an f"…" pushed.benches/template.nika drops its ref String; ledgers of examples and tools/*.nika regenerated and each change looked at; lower-std diff committed.crates/nikaia/tests/measure.rs gets the A/B of the issue body as a measurement.--test specification; regenerate tests/specification/EXPECTED.txt / COMPILES.txt only for what this changes).Generated by Claude Code
Steps 1 and 3 built in 9b428ef, version 0.0.1036 (7623620).
if/match), a let, a parameter or a list element into which only literals flow is a fourth tier (Tier::Lasting) and lowers to &'static str: no String::from, no lifetime parameter. benches/template.nika without ref lowers with no copy per row. With text of its own beside the literals, they are built as before.Vec() handed whole into a position takes that position’s type and tier: names.push("Ada"); Team { names: names } compiles (NK1106 before), with or without an f"…" pushed.push_str, +=, a mut parameter, a reassigned name) stays text of its own. Where the walk cannot see where a static view goes (a tuple, an enum payload, another file’s function or struct, a ref container, a list pattern), the position falls back to owned.m[k]?.clone() on a map of views lowered to to_owned() (a &&str copy); now to_string() (text_copies). On the rebase onto 0.0.1030: s.bytes handed back from such a parameter is copied (ADR-320 D11), not NK1131.text_tiers.rs; handed_over.rs programs that meant text of their own now write "b".clone(). Re-run after the rebase: language, borrowed_subject, stored_views, result_source, expr_ids, entry_points, sequence_shapes, function_types, text_tiers, handed_over, selfhosting_defects, sysroot, examples, specification, bytes, grammar_bytes, text_bytes, grammar_lowering — all green apart from text_bytes, which is fixed as above.CxByValue is filled with literals in cexport_model.nika and with owned text in cexport_node.nika, so a view there broke lower-std. Today a pub position stays String. To build: run the walk over the whole package (all files of the unit together), then a pub position only literals flow into is a view in the ledger. Test: one pub field of each kind across two files, ledger lines checked.&'static str today: HashMap<&str, V> looked up with a String failed rustc (&str: Borrow<String> does not hold). To build: look up with .as_str() (&'static str: Borrow<str>), then let keys and values only literals fill be views. Test: a map filled with literal keys, read with an owned key and a literal.null. if c { "a" } else { null } stays String? (the checker types it owned, arms_meet_at_null). To build: a view Option<&'static str>. Test: that probe.benches/, examples and tools/*.nika; measure.rs; the specification’s expected files).Generated by Claude Code
Item 2 (map keys and values) is built in c8cddf7, version 0.0.1060.
String key or value is decided by what flows into it: views make it a view, only literals make it &'static str, text of its own makes it String, both make it EitherText. The in_maps seeding of never is gone.for (k, v) in m.drain(), .iter(), .into_iter(), m) binds the key and the value separately, each with the kind of its own position. A key put back into the position it came from adds nothing.s.bytes[a..<b].text() is read as Bytes::text, so it gives a view.nikaia_std::index::as_text(&k). This covers brackets, get, contains_key and remove. There is a new FactKeyForm::Viewed.examples/1brc.nika, access-log and k-nucleotide drop ref from their map keys and lower byte for byte as before. Tests are in text_tiers.rs.Still open:
EitherText-keyed map looked up with an owned String still fails in rustc (EitherText: Borrow<String>).Found on the way, filed separately:
let x: String? = f() in a loop, then stored, lowers through the keep frame.split_once are not followed by the tier walk.Generated by Claude Code