??’s Answer Is Only Read, a Value That Is a Word by Its Kind Is Copied and Everything Else Is LentStatus: Superseded by ADR-279 — its D1-D2 are D8-D9 there
Date: October 3, 2026
Answers: what ?? does with a value of a type that copies when the
answer is only read: lend it, as ADR-259 D1 says of every read
position, or copy it, as a number is copied. Raised by 0.0.377, which
changed the lowering for such types as a side effect of a different fix; the
owner chose option B.b.
Related: ADR-259 D1 (?? lends its left side where the answer
is read and takes it where it is kept; “nothing is copied” in a read
position); ADR-233 D4 (a view of a value that copies is read as
the value); ADR-152 D2 (an array copies as its elements do);
ADR-008 D5 (the compiler inserts no copy of a value that does
not copy); ADR-258 (the four weights)
A struct or an enum whose parts all copy is a copy type: below it
derives Copy, and handing it on leaves the original usable, as a number
does. Grid { cells: Array[f64, 8] } is one, 64 bytes wide, and so is
Row { id: i64 }, 8 bytes wide.
fn total(g: ref Grid) -> f64 { … } // only reads its argument
let found: Grid? = find(1)
let spare = Grid { cells: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
println(f"{total(found ?? spare)}") // a read position (ADR-259 D1)
println(f"{found == null}") // `found` is used again
When the answer of a ?? is only read, and the value is of a copy type,
does the compiler lend it or copy it? The program means the same in both
cases. The answer is only read, and the original stays usable either way, so
no program changes its behaviour. What changes is what the program costs, and
whether ADR-259’s sentence holds as written.
How it came up. Until 0.0.376 the checker knew only the language’s own scalars and the ledger’s types as copying. A copy type a program declares was read as one that moves. ADR-259 D1 therefore lent it in a read position:
total(nikaia_std::index::or(found.as_ref(), || &spare)) // until 0.0.376
0.0.377 taught the checker that such a type copies, because handing one on
was refused as a move (NK2105, NK2106) in programs that are correct. The
same knowledge reached ??. ADR-259’s lending is asked only for types that
move, so the read now copies both sides into a temporary and lends that:
total(&nikaia_std::index::or(found, || spare.into())) // since 0.0.377
Neither is wrong. The choice between them was never made on purpose, and ADR-259 does not decide it, because it was written about types that move.
| Language / tool | Its answer | What it costs there | Source |
|---|---|---|---|
| Rust | the program chooses: opt.unwrap_or(x) copies, opt.as_ref().unwrap_or(&x) lends; Clippy advises by size - trivially_copy_pass_by_ref (a small Copy value passed by reference should be passed by value) and large_types_passed_by_value (a large one by value should be passed by reference, default 256 bytes) |
the developer writes the choice at every use | Clippy’s lint list; limits from memory, not checked against a version |
| C# | ?? on a nullable struct gives a copy; in and ref readonly parameters exist so that a large struct is not copied; the guidance is to keep structs at 16 bytes or less |
a copy at every read of a large struct, unless the program writes in |
.NET design guidelines, from memory |
| Swift | value types are copied by meaning; the optimiser removes copies it can prove unneeded, and large storage is copy-on-write | a large inline struct is copied where the optimiser cannot see through | from memory, not checked |
| C++ | c ? a : b over two lvalues of one type is an lvalue: bound to a const T&, nothing is copied |
none; the program writes const T& |
the standard’s conditional operator, from memory |
| Kotlin, Java | objects are references; the question does not arise (a value class has one field) |
none | - |
What transfers. Every language that has large value types either leaves
the choice to the program (Rust, C++, C# with in) or relies on its
optimiser (Swift). The size rule exists as advice in Rust and C#. What
does not transfer. Here the program does not write the choice: whether an
argument is lent is the compiler’s (ADR-094 D1), and ADR-259 applies that to
??. So the compiler has to answer it.
Measured (the lowerings above in rustc -C opt-level=3, a function that
only reads its argument). For the 8-byte Row the two lowerings compile to
the same instructions, and the optimiser merges the functions. For a
512-byte struct the lent form is 4 instructions with no copy; the copied form
reserves 512 bytes on the stack and calls memcpy at every read.
A copy type is read as a number is: in a read position ?? copies.
total(found ?? spare) // copies `found`'s value or `spare`, then lends the copy
ADR-259 D1 as written - in a read position nothing is copied - with an exception for the values a reference can only make dearer. Three ways to draw the exception were looked at:
B.a — only the language’s own scalars are copied. A number, a bool and a
char are copied; everything else is lent. It lends a 1-byte enum, which
costs a pointer and a load where the byte itself would do.
B.b — what is a word by its kind is copied. A number, a bool, a char,
an enum whose variants hold nothing (and a T? of one), and a type the
ledger says copies (winnow_grammar::Symbol) are copied: each is at most one
machine word because of what kind of type it is. A type made of parts - a
struct, a tuple, an array, an enum with a payload - is lent, whatever its
size. No size is computed.
B.c — the compiler computes the size. Copy a copy type up to two machine words (16 bytes), lend a larger one: the line Rust’s Clippy and C#’s guidance draw. The compiler works the size out from the parts - alignment, an enum’s largest payload and its tag.
What each copies (sizes measured on x86-64):
| Type | Size | A | B.a | B.b | B.c |
|---|---|---|---|---|---|
a number, i8 to i128, f32, f64 |
1-16 | copy | copy | copy | copy |
bool, char |
1, 4 | copy | copy | copy | copy |
an enum whose variants hold nothing (Kind { A, B }), and Kind? |
1 | copy | lend | copy | copy |
a ledger copy type (winnow_grammar::Symbol) |
4 | copy | lend | copy | copy |
a small struct, tuple or array (Row { id: i64 }, (i64, i64), Array[f32, 4]) |
8-16 | copy | lend | lend | copy |
a larger one ({ a, b, c: i64 }, Grid { cells: Array[f64, 8] }) |
24, 64 | copy | lend | lend | lend |
an enum with a payload (Shape { Dot, Circle(f64), Rect(f64, f64) }) |
24 | copy | lend | lend | lend |
total(found ?? spare) // `Grid`: A copies 64 bytes; B.a, B.b and B.c lend
named(kind ?? Kind::B) // `Kind`, 1 byte: B.a lends it; A, B.b and B.c copy it
| D1 the special case pays, not the common one | D2 the compiler works, not the developer | D3 no cost for the common case | D4 consistent with itself | If it is wrong | |
|---|---|---|---|---|---|
| A | a program reading a large copy type pays a copy it never asked for | nothing to write | a memcpy per read for a large copy type (measured) |
a copy type reads like a number; ADR-259’s “nothing is copied” holds only for types that move | a lowering change; no program changes meaning, only cost |
| B.a | a program reading a 1-byte enum pays a pointer and a load | nothing to write | no memcpy; a reference to a byte where the byte would do |
ADR-259 D1 as written, with the language’s scalars as the one exception ADR-233 D4 already makes | as A |
| B.b | nobody pays measurably: a word is copied, a struct of 8 or 16 bytes is lent, which compiles to the same instructions as its copy (measured) | nothing to write; the compiler asks the kind of a type, which it already knows | no memcpy at any size; no reference to a word |
one rule a reader can see in the type: made of parts, lent; a word by its kind, copied | as A |
| B.c | nobody pays | nothing to write; the compiler computes sizes, alignment and enum layout | as B.b | a line a reader cannot see in the source, and a size the language below does not promise for its own layout | as A, and the computed size may disagree with the one below at the line |
B.b. It is ADR-259 D1 as written, with the one exception that makes a reference strictly dearer: a value that is a word by its kind. It needs no size, so the rule is visible in the type and cannot disagree with the layout below.
??’s answer, the left side is lent unless its type is a word by its kind.A word by its kind is a number, a bool, a char, an enum whose variants
hold nothing, or a type the ledger says copies; such a value is copied, as a
number is (ADR-233 D4). Every other type - one that moves, and a copy type
made of parts: a struct, a tuple, an array, an enum with a payload - is lent,
at every size.
A copy type handed on whole is not taken: NK2105 and NK2106 stay silent
about it, as for a number. This record is about the read position only.
Why not A. It makes a program pay for a size it cannot see at the use: a
memcpy per read of a large copy type, which ADR-259’s read position exists
to avoid. Why not B.a. It lends a 1-byte enum, the one case where a
reference only costs. Why not B.c. It buys nothing over B.b that can be
measured - a struct of 16 bytes or less lent compiles to the same instructions
as its copy - and it costs a size computation whose answer the language below
does not promise for its own layout.
??, as it
was until 0.0.376: or(found.as_ref(), || &spare). A word by its kind keeps
the copy. No program changes its output or its validity.?? is the one position this record is about. The same
question at every argument would be its own record, if a program measures a
cost there.