ADR-318: An Option’s Default Is a Build-Time Value — Evaluated Once Where the Function Is Declared, Recorded as Its Value, and Handed to the Call in the Form a comptime Crosses In; a std Type the Compiler Cannot See Into Crosses Through the Parts Its Ledger Entry Names

Status: Accepted Date: October 4, 2026 Answers: #374 (an option whose type is time::Duration cannot have a default), the owner’s choice of its first direction Related: Part I 5.1 (options and their defaults); ADR-287 (what a build-time body may do; D10’s one evaluator, D13-D14’s rule, D18’s interpretation); ADR-311 (what a build-time value may hand over, and in which form); ADR-150 D1-D2 (time::Duration is Rust’s own type; a span is written 5.seconds() and has no second spelling); ADR-251 D4 (the ledger’s signature is spelled as Nikaia spells a declaration); ADR-296 D17 (the emitter knows no types); ADR-123 (crosses, a column for a type whose parts the compiler cannot walk); ADR-294 D18 (std’s Nikaia half)

1. Question

What may an option’s default be, when is it computed, and how does it reach a call that leaves the option out?

The language below has no defaults, so a left-out option is written into the call by the compiler, from the callee’s ledger entry. A literal makes that trivial: the ledger holds its text, and the text is the value in both languages. A span of time is not a literal — 30.seconds() is a call — so an option whose type says its unit cannot have a default, and the unit moves into the parameter’s name over a bare number.

The question is general. Duration is the first type that needs it; any value that is not a literal raises the same three things: when it is evaluated, what the ledger records, and how the call writes it.

2. How others do it

Language A default is Evaluated What it costs there
C++, Swift any expression at each call, at the call site the expression is part of the interface; a header or module interface carries it
Kotlin any expression at each call, inside the callee a synthetic $default function and a bitmask per call
Python any expression once, when the def runs one shared object per default — the mutable-default trap
C# a compile-time constant while compiling the callee the constant is copied into each caller; changing it needs callers rebuilt
Rust — — no defaults; a builder or Option<T> instead

What transfers: C#’s shape — a constant, recorded with the callee, copied into the call. What does not: an expression evaluated per call, which makes a default something the ledger cannot show and a caller cannot see.

3. Options, weighed

Option Why it lost
Keep a default is a literal; an option needing more takes a T? and decides in the body the unit leaves the signature and the default leaves the ledger, which is what a caller reads
Record the expression and evaluate it in each caller’s build the caller needs the callee’s bodies, which a ledger does not carry; and cli::args is a build-time input (ADR-287 D14), so two packages could compute two defaults for one declaration
Evaluate at run time, per call (Kotlin, C++) the default is no longer a value anyone can be shown, and a default may then do things
Put Duration on ADR-311 D1’s list one case in the compiler per std type; the next type is the same question again
Read an opaque type’s parts from the Rust type’s fields Duration’s fields are private to Rust’s std, and the language below promises no layout (ADR-311 D16)
Give the evaluator a table of std functions it implements itself a hand-kept allowlist, which ADR-287 rejected because it rots

4. Decision

D1 — A default is a build-time value: what a comptime’s initialiser may be.

A literal stays the common case. Beside it, a default may be anything Part II 10.2 lets a comptime hold, evaluated under the same rule (ADR-287 D13-D14) by the same function (D10): timeout: time::Duration = 30.seconds(), a struct literal, a call to a function of the program, an item-level comptime of the package. A default is a comptime without a name.

D2 — It is evaluated once, while the package that declares the function is built.

Not at the call, not at run time. The declaration is where the author sees the value, and the build that has the bodies is the one that can evaluate them.

D3 — The ledger records the value, not the expression.

The signature column writes the evaluated value: a literal as today, a struct as a struct literal of literals, an opaque std type as D5’s constructor over literal parts. A caller reads a value and copies it, as it copies a literal now; it evaluates nothing, so it needs no bodies and no build parameters of the callee’s package. A ledger whose defaults are all literals is unchanged.

D4 — It reaches the call in the form a comptime crosses in.

ADR-311 is the rule, unchanged: a value arrives in its view form, a value that owns memory is refused by what it is (NK1167), a struct and an enum variant are written field by field against their declaration. The compiler writes it into each call that leaves the option out, as it writes a literal now.

D5 — A std type whose parts the compiler cannot walk crosses through the parts its ledger entry names: the constant column.

A type entry may carry constant: a constructor and the parts it takes, each of a type that itself crosses.

[type."time::Duration"]
constant = "time::Duration::new(secs: u64, nanos: u32)"

The build-time evaluator holds a value of such a type as its type and its parts; the crossing writes the constructor over the parts as literals. The constructor is an entry of std like any other, a const fn in std’s Rust half so that a comptime of the type is a const too, and not pub: it is the type’s definition for the compiler, not a second spelling for a program (ADR-150 D2). A std type whose entry has no constant does not cross, and is refused with NK1167 naming the type. It is the column ADR-123’s crosses is for threads: an answer for a type whose parts are the library’s, written once in std and never a case in the compiler.

D6 — A std function runs at build time when the ledger lets it.

Changed by ADR-321 D1-D2: build-time code is compiled, so a std function runs whatever language its body is in.

Build-time code is compiled against std (ADR-321), so 30.seconds() runs from std’s Rust half and is the same value whether the build computes it or the program does. What decides is ADR-287 D13-D14’s rule, read off the ledger — never a table of functions an evaluator implements beside std.

D7 — A default is refused with the codes a comptime is, at the default.

NK1127 cannot evaluate it, NK1152 the rule forbids a callee, NK1167 it owns memory or names a type with no constant, NK1168 a ring through an item-level comptime. The span is the default, and the message names the option. The parse no longer refuses a default for not being a literal.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.