ADR-274: Where ??’s Answer Is Only Read, a Value That Is a Word by Its Kind Is Copied and Everything Else Is Lent

Status: 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)

1. Question

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.

2. How others do it

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.

3. Options, weighed

Option A — copy every copy type

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

Option B — lend, with an exception for small values

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

Weighed

  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

4. Decision

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.

D1 — In a position that only reads ??’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.

D2 — What 0.0.377 decided about keeping stays.

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.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.