String?, a List Going in Whole, and the Foreign BoundaryStatus: Superseded by ADR-282 — its D1-D4 are D12 (with D22 and D25), D14 (with D5), D16 and D27 there
Date: September 26, 2026
Target Version: Nikaia 0.0.207
Answers: #200’s remainder after
ADR-223: a mixed position reached only through an f"…" hole, a
nullable String?, a container initialised whole with elements of both kinds,
and ADR-107 D5’s foreign-boundary copy — under the owner’s Nikaia does not
build walls, it takes them down, and the special case pays, not the general
one
Refines: ADR-223 D2 (its hole exception) and §3 (three of its
five rows); ADR-222 §3 (the String? row)
Related: ADR-107 D3 (a copy is written, never inserted) and
D5, ADR-207 D3 (a String the body only reads is a &str),
ADR-068 (the nullable wrap)
ADR-223 left four things. Probing them found that two were worse than refused:
struct Entry { note: String? }
let e = Entry { note: text.lines().next() } // rustc: Option<String>: From<Option<&str>>
let mut b: Vec[String] = text.lines().collect() // rustc: mismatched types, once b is mixed
let mut c = words(text) // a list of views, no annotation
c.push(f"tail") // rustc: expected `&str`, found `String`
Each passed the checker and failed in rustc, about a file nobody wrote
(Part III C.1). A String? was not a position, and the checker did not look
through the ? when asking whether a view is kept.
String? is a position, and null is no text of either kind.Everything ADR-222 and ADR-223 decide for a declared String holds for a
declared String?: field, result, parameter, annotated let, and the element
of a list. Views only make it ref String? (Option<&str> below), both kinds
make it EitherText that may be absent. null puts nothing into the decision.
A published String? is never both kinds, as D4 of ADR-223 has it.
And the kept-view question is asked through the ?: a view put into a String?
that stays text of its own is refused with ADR-208 D2’s explanation, where
before it reached rustc.
nikaia_std::either_text::IntoEither hands a value over as it is: a view
borrowed, text of its own moved, Option of either kept Option.
IntoEitherMaybe does the same into a String?, present or absent whatever
the value was. The pass that decides tiers writes value.into_either() (or
.into_either_maybe()) into the program; the emitter writes
either(value) where it wraps itself (a field, a result, an argument). The
checker reads both as the value’s own type, so the nullable wrap ADR-068
decides is written around the conversion — Some(either(v)) — exactly as
it would have been without it. No text is copied.
A hole no longer keeps a position text of its own. An argument is wrapped
by the emitter, which writes the hole’s code as it writes any other, so a call
inside f"{tag(line)}" hands each kind over like a call anywhere else. And a
parameter the callee only reads is a &str below whatever flows into it
(ADR-207 D3): every caller lends what it has, so nothing is wrapped for it at
all — the tier decides only what a keeping parameter is.
[first, f"own"].collect into a list whose element is mixed hands over each item as it
is: lines.either_items().collect(). The list is built once.let c: Vec[String] = words(text), followed by c.push(f"tail"), makes
words hand back a list of EitherText, and nothing is converted at the
let. The line that pays is the one that mixes: words’ own pushes are
wrapped, its readers read text as before.let mut c =
words(text) shares words’ result position, so a push into c decides it.A converted copy of the list at the let was the other way. It would allocate
on every execution of that line with nothing in the source to show it, which
is the copy ADR-107 D3 rules out for text, one level up.
A described crate’s entry says ref String or String in the ledger’s
language (ADR-104). A view crosses to ref String for free. Crossing to
String — text the crate may keep — is refused with NK1102, which says the
crate keeps its parameter after the call and names .clone(). The copy is the
program’s to write, never inserted. This was already the checker’s behaviour
once entries existed; it is recorded and tested here. A described position is
not the package’s to decide, so it gets no tier: its representation is the
crate’s.
push, insert or m[k] = v written inside
a holeString — not a position.rustc).collect. Not a tier question: Nikaia’s collect
builds a list, and the checker accepting it into a declared map is a defect
of its own (#208).#200 keeps only the published case and a push inside a hole.
The README’s wall row names the published case alone. Programs that compiled
before compile to the same Rust except that a value going into a mixed
let or list is written value.into_either() where 0.0.206 wrote
value.into(), and one going into a mixed field, result or argument
either(value) where it wrote EitherText::from(value).
All of it, in 0.0.207:
nikaia_std::either_text: IntoEither, IntoEitherMaybe, EitherItems,
either, in the prelude.crates/nikaia/src/text_tiers.rs: String? positions, null, list literals,
collect, links between positions, a list bound without a type.?, at a field, a let and a result.&str; null never wrapped.crates/nikaia/tests/text_tiers.rs: a keeping parameter reached from inside a
hole; a String? field, result and let of views; one both kinds flow into,
with a Vec[String?]; a list literal, a collect, a list bound with and
without a type; the published String? refused. Each run at both settings
of user_parallelism. crates/nikaia/tests/described_entries.rs: D4.