Status: Accepted Date: October 14, 2026 Answers: what a result points into when it may view more than one parameter, and what a caller may do with an argument while it holds such a result Related: ADR-283 D3 (no implicit copy), D13 (the walk), D23, D24 (the keep), D25 (changed by D1-D3 here); ADR-251 D4 (the ledger’s spelling); ADR-282 D13-D16 (text tiers: a position is what flows in); ADR-001 D1 (stable toolchain); Part I 6.5, 6.6, 6.8; Part III 13.5, C.3
Where does a result point when the body can hand back views of two parameters, and what does that cost the caller?
struct Pair { left: ref String, right: ref String }
fn pair(a: ref String, b: ref String) -> Pair {
return Pair { left: a, right: b } // each field views one parameter
}
fn longest(a: ref String, b: ref String) -> ref String {
if a.len() >= b.len() { return a }
return b // one of the two, chosen at run time
}
Nikaia promises that nobody writes or reasons about lifetimes: the compiler reads off the body where a result points (Part I 6.5). A rule that refuses these functions, or ties every view of a result to every parameter, puts that reasoning back on the programmer.
All rows are from memory, not checked against a current manual.
| Language | A value viewing several inputs | One of several, at run time | The owner changes while viewed |
|---|---|---|---|
| Rust | one lifetime per field if written by hand (Pair<'a, 'b>), else one shared |
allowed with one written lifetime 'a over both |
refused: the owner is borrowed; a reassignment drops the old value, so it is refused too |
| Swift (lifetime dependencies) | a dependence on several sources | a union of sources | refused while the dependent value lives |
| Mojo (origins) | an origin per value | a union, ref [a, b] |
refused while the origin is in use |
| C++ | unchecked | unchecked | a dangling reference |
| GC languages | the collector keeps every input alive | same | the old value lives on while referenced |
Every language with checked references accepts the run-time choice. None of them lets an owner give its value up while it is viewed; a collector does, by keeping the old value alive, which is what a tether does without one.
| Option | Why it lost, or won |
|---|---|
| Refuse a result that may view two parameters (ADR-283 D25) | the programmer splits the function or copies: the reasoning about where a view points, done by hand |
| One source set for the whole value | sound, but a view taken out of one field keeps every other field’s source too: a caller holding p.left may not change b |
| A source set per view, unions where the body chooses at run time | chosen (D1, D2) |
Name the sources in the signature, -> ref(a, b) String |
a lifetime with a parameter’s name, the annotation the source never has (Part I 6.5); the ledger keeps the spelling |
| A copy where a caller would conflict | an implicit copy (ADR-283 D3) |
| Refuse every use of an owner while a view of it lives, as Rust does | refuses programs whose data is never in danger: giving a value up moves its buffer, it does not destroy it, where a keep can hold it (D3) |
| Copy-on-write when the owner changes in place | an implicit copy (ADR-283 D3) |
Every view a result holds, the result itself or each view inside it (a
field, an element, a variant’s value), points into the parameters its ways out
reach, by ADR-283 D13’s walk. The sources are kept per view, not per value:
pair hands back a Pair whose left points into a and whose right
points into b, and a view taken out of left points into a alone.
Changed by ADR-views-3 D8: where there is no body (a trait’s method, a function type, a foreign function) the source is written from and required, not taken as the union.
A view that may come from one parameter or another, by two ways out or by one
value assigned either, points into each of them: longest’s result points
into a and b. The function is not refused. A trait’s method or a function
type, which has no body, hands back a view of the receiver, or else of every
parameter that is a view.
While a caller uses a view that may point into an argument, the argument’s data stays as it was. Giving the value up is not changing it: a reassignment of the variable, the end of its scope, a move into another variable of the same function, or handing it to a parameter that is lowered as a view (Part I 6.5: the callee neither keeps nor changes it) moves the old buffer into the keep of whatever holds the view (ADR-283 D23), and it is freed when the last such view is gone. No copy is made (ADR-283 D3). A variable the value moved into is followed as the argument was: changing it in place is D4’s.
A change in place - a method that changes the value, an assignment to a part
of it - of data that a view still to be used may point into is refused with
NK2305, at the change. The message names the view, the call that made it,
and the way out of the callee that may hand back that argument; it speaks of
data, never of how long anything lives. The ways out it names: use the view
before the change, or take a copy of it with .clone() before the change.
Handing the value to a callee that takes it as its own - a parameter that is
not lowered as a view, because the body keeps it or changes it, push on a
list among them - while a view that may point into it is still to be used is
refused with NK2305, at the call. The new owner may change the data or free
it, and the caller cannot see when. The message says so in the words of D4:
the headline names the value and the view, the labels mark the call that made
the view, the hand-over (take keeps y and may change or discard it) and
the later use. The ways out it names: use the view before the hand-over, hand
over a copy (take(y.clone())), or keep a copy of the view
(longest(x, y).clone()).
Changed by ADR-views-3 D10: the ledger writes a source as the source does, -> String from a | b, returns = "left: from a, right: from b".
A result that is a view writes its sources in the signature,
-> ref(a | b) String (ADR-251 D4). A result that holds views writes each
view’s sources by its path, returns = "left: ref(a), right: ref(b)". A caller
reads this and never the body.
A struct keeps apart what each of its fields points into: each field that
holds a view, directly or through another type, has a place of its own, in the
order the fields are written. A field whose type itself holds several views is
one place: what its views point into is joined (a returns path is one field
long, D5). The ledger’s tethered lists these fields in that order, and a
package that reads the type takes its places from that list; nothing else is
written. A pub type has the same places as a private one.
An enum has one place for all its views: a value is one of its variants, a
choice made at run time, which points into the union (D2). A type the program
throws keeps one place as well (its sum, ADR-280 D15); not decided here
whether it should have more.
Added October 29, 2026 (#942, #945).
let v = parse(text) // v views text
let mut c = Cursor { input: text, pos: 0 } // text moves into c.input
c.pos = c.pos + 1
println(v.head)
A struct literal, a variant’s value, a tuple or a list built in a function is a variable of that function in the sense of D3. Moving a text into one of its positions while a view of that text is still to be used is giving the text up: its buffer moves into the keep of what holds the view, and the position holds a view of that buffer. The move counts as a view flowing into the position (ADR-282 D13): the field, element or payload is a view where only views and literals flow in, and holds either where text of its own flows in too (ADR-282 D14), across the whole program, as for a view handed in directly. The new place is followed as the argument was: changing it in place while the view lives is D4’s.
The same holds for a text parameter whose body only moves it into such a local value: that is not keeping it (Part I 6.5). The parameter stays a view, the position becomes a view of the caller’s argument, and views the function hands back out of the value point into that parameter (D1, ADR-283 D24).
NK2305.A position holds text of its own where the program changes it in place, hands
it to a callee that keeps it, or keeps it where only text of its own may stand
(Part I 2.2) - anywhere in the program, not only in the function that builds
the value. Moving a text whose data a view still to be used may point into
into such a position is handing it to a new owner (D6) and is refused with
NK2305, at the move. The message is D6’s: the headline names the value and
the view, the labels mark the call that made the view, the move into the
position (Cursor.input must hold text of its own), the place that makes
it so (c.input.push_str(..) in advance changes it in place) and the
later use. The ways out it names: hand over a copy
(Cursor { input: text.clone(), .. }), or build the value first and take the
view from its field (let c = Cursor { input: text, .. }; let v = parse(c.input)).
NK1106 (a view stored where text of its own is needed) and NK2105 (a value
used after it was handed over) keep their cases where no view of the moved
text is still to be used.
A struct, a variant’s value, a tuple or a list handed back from a function, whose views point into a buffer the body made or a text moved into it, puts that buffer in the keep of the frame that keeps the result (ADR-283 D10, D23), as a single view handed back does. The result is not refused and its fields are not made text of their own for it.
pub type (D1 would stop at pub,
and a word that only widens who may read a type would change what a program
means); places grouped by which fields the package’s functions fill from one
source (fewer places, but a new function elsewhere would change the type for
every package that reads it). Adding a field that holds a view changes the
places, as it already changes fields and tethered: the ledger diff shows
it.let: every change to it is in view, so D4 covers it, which is D6’s own
reason for not refusing a move into a variable. The tiers already decide a
position by what flows in; a moved text is a view of a kept buffer, so it
flows in as a view. A parameter only moved into such a value is lent, as one
only read is. Rejected: a literal as a new owner, refused as D6 (refuses
programs whose data is never in danger, and leaves
let moved = text; Cursor { input: moved } inconsistent); a counted shared
buffer (Swift; a count on every move and copy-on-write, rejected in D6); the
compiler inserting the copy (ADR-283 D3); an unchecked view of the heap
buffer, which survives the move of a String (unsafe in emitted views,
against ADR-283 D14, and unsound for a text with inline storage); a
self-referential owner-and-views value (a crate’s unsafe, and it fuses
unrelated locals); the compiler reordering the statements (changes the order
of calls that may have effects). Waiting for Rust’s next borrow checker: it
accepts no shape of this question the current one refuses, a moved value
borrowed from is not on its roadmap, and the toolchain uses no unstable
feature (ADR-001 D1).NK1106 or NK2105 here (they speak of
a view or of a use after a hand-over, not of the data the live view needs,
and name neither the view nor the reorder).NK2302 no longer refuses a function for what its result points into; it
keeps its other cases (a parameter kept where nothing names its buffer).NK2305 is new: a change in place of data a view still to be used may
point into, or its hand-over to a callee that takes it as its own (D6).--tethers.NK2401).tethered carries meaning, so the ledger’s version is 4
(D7).NK2305, and the message names that
place (D8, D9).--tethers or a lint names a buffer kept only because its owner
moved on.