impl and the BoundStatus: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: Part I 4.6’s struct Box[T] and Part I 4.7’s
trait Summarize { … }, both printed and neither compilable;
#193 (a bound across a
package, steps 1-4); #162
and the former open-decisions.md’s how does a bound reach a
caller across a package boundary?, decided by the owner in favour of that
page’s recommendation
Related: ADR-024 D4 (the erasure D1 keeps where its reason
holds), ADR-288 (a signature binds from the receiver — D2 points
it at the arguments), ADR-296 D17 (the emitter resolves nothing),
ADR-080 D1 (NK1130 and NK1129: what an impl owes its trait),
ADR-288 D24 and D25 (a trait method may pause unless it says
sync), ADR-286 (a trait reached across a package is imported by
the emitter), ADR-286 D11 (use brings no name in),
ADR-286 D1 (naming is per package), ADR-010 D1
(absence is never an answer), ADR-023 D3 (the trait the compiler
reads rather than one a .nika file wrote), ADR-100 (a consumer
reads a dependency’s ledger; the hash and --locked), ADR-251 D4
(the ledger speaks Nikaia’s syntax), ADR-304 D2 (a shape bound,
[T: Struct], answered by a declaration), Part III C.1, C.2 and C.4
What does a type parameter mean, what may a bound on it say, and who answers for the bound — in one file and across a package?
Part I 4.6 and 4.7 printed generics and traits before either compiled. The
[T] was read by the parser, erased by the checker and dropped by the emitter,
so fn hand[T] reached rustc as a type nobody declared. trait Summarize
was a parse error at the keyword, so a bound had nothing to name. And once a
bound existed it did one of its two jobs: it put a method in reach of the body,
while nothing asked which types may stand here — so tell(rock) where Rock
implements nothing, and a call into a package’s generic function, were both
answered by the backend about a file nobody wrote (Part III C.1). Each step is
the same question asked one place further out: the body, the declaration, the
call, the package.
Measured against the one language this lowers to; no other was surveyed.
| Language / tool | Its answer | What it costs there | Source |
|---|---|---|---|
| Rust | <T: Trait>, monomorphised; a body may use only what a bound declares, a caller must satisfy the bound |
no dispatch at run time; one body per instantiation at compile time | measured |
| Rust, the parameter alone | fn shout<T>(x: T) -> String { x.to_uppercase() } |
refused: no method named to_uppercase found for type parameter T |
measured |
| Rust, two bounds with one method | the call is ambiguous and needs a disambiguation | a syntax Nikaia cannot write | §6 |
What does not transfer: Rust has dyn, where clauses and default bodies,
and Nikaia has none of the three; a trait here names a bound and nothing else.
The number that decided it: rustc compiles 60 generic functions at 8
instantiations 11 % faster than the same 480 written out, and the generated
generic runs as fast as its concrete twins - the cost to watch is the
instantiation count, not the feature.
| Option | Lost because |
|---|---|
| Keep ADR-024 D4’s erasure in the body as well as at the call | T became ?, ? fits everything, so a body that misuses its parameter checks clean and fails below — C.1 |
Emit the <T> and nothing else |
turns every generic fails into every generic whose body uses its parameter fails: the same class one size smaller |
Infer T from its uses |
a constraint solver, with pathological inputs; a parameter that is written leaves nothing on the right-hand side to solve for, which is what keeps the cost flat |
impl[T] Stack[T], a second list |
a spelling Part I does not have, bought to avoid a rule that reads the list already written |
A trait method as an Item::Fn with an empty or optional body |
declares this and does nothing become one shape, and an absent body could be emitted by accident |
| A bound as a licence that the member exists | arity, argument and result types, sync and throws would go unchecked; one lookup answers all of them |
use http bringing Handler in |
breaks ADR-286’s every name on a line says where it comes from; a path in the bound makes the exception unnecessary |
| One ledger listing a trait’s implementors completely | an impl may be written in the trait’s package, the type’s, or a consumer’s; a list read as complete turns absence into an answer (ADR-010 D1) |
| Who implements what, in the checker’s per-file walk | one file sees one file, so a two-file project that builds and runs would be refused — C.4, the most expensive failure |
| The bound in a ledger key of its own | a second key that must agree with the signature is a second source of truth for one fact |
| The bound recorded and read as the package spells it | the consumer’s trait map holds handler::Handler; a bare Handler finds no trait and the refusal never lands |
The weighing in every row is the same one: a correct program refused (C.4) is the worse mistake, and a wrong one passed to the backend (C.1) is the one to close — so each refusal is placed where the compiler has the answer, and withheld where it does not.
In the body T is a type and is compared as one: the caller picked T, so a
body that puts an i64 where T is wanted is wrong. At a call T is a
variable, bound from what the caller passes and substituted away, or ? where
nothing bound it; it never survives into a comparison, so no caller is told
that i64 is not T — ADR-024 D4’s reason, kept exactly where it
applies. Self stays erased: no call site binds it.
hand(n) with n: i64 is an i64; Pair { first: n, second: n } is a
Pair[i64], and a value’s own type arguments bind its declaration’s parameters
when a field is read back. One structural walk, the one
ADR-288 uses for a receiver: no queue, no fixpoint, no occurs
check, because a type parameter is declared and never solved for. A mismatch
binds nothing.
fn, a struct and the impl over it.fn hand[T] is fn hand<T>, struct Pair[T] is struct Pair<T>,
impl Pair[T] is impl<T> Pair<T>; an input lifetime stands first,
struct Row<'a, T>.
impl’s parameters come from the slots it already writes; there is no impl[T].impl Stack[T] declares T and impl Stack[i64] applies i64: a bare name
that is neither a Part I 2.2 type nor one this file declares stands for a type.
The rule lives in one function that the ledger, the checker and the emitter all
call.
NK1126: a member reached on a type parameter that no bound of it declares.Fields and methods both. The message says no bound, not no such method: the method may exist on every type a caller passes, and what is missing is the sentence that says so. It is asked after the bounds (D8), so it fires only where none of them declares the member.
trait Summarize { fn summary(ref self) -> String } is an item whose methods
have no body — not an empty one and not an optional one — and the emitter
writes it with a writer for declarations rather than by omitting a definition’s
parts. trait is a reserved word.
[T: Summarize], and several are [T: A + B].Inside the brackets Part I 4.6 already puts the parameter; + joins a list.
They reach the generated file as <T: Summarize>, in source order.
A trait’s methods are recorded as Summarize::summary, the key shape an
impl’s methods get, because a bound and a receiver ask one question: what
does a value of this thing have. A member reached on a type parameter is asked
of its bounds as receivers, and the trait’s entry answers arity (NK1101),
argument types (NK1102), the result type, sync, throws and the binding of
the signature’s variables. A name that is not a trait in the ledger names none.
A declaration has no body to infer from, so what it says is what it is: sync
where the word stands, and without it a method that may pause, as a
function type reads (ADR-288 D24). A call through a bound pauses
where the declaration allows it.
fn dispatch[H: http::Handler](r: Router[H]) and [T: A + http::B]. The
grammar’s rule for a type name is the rule for a bound’s trait name.
use is unchanged: no name comes in, for a trait as for anything else.trait table, and a trait’s methods are the ordinary fn entries beside it.[trait."Handler"]
pub = true
[fn."Handler::handle"]
signature = "(ref self, request: Request) -> Response"
sync = true
The table carries the trait’s name and nothing else — no fields, and no list
of methods, which would be a second source of truth for the fn entries the
impl owes (NK1130). The table is what a bound and an impl … for must name,
and a trait is never the type of a value.
impl is an entry in the ledger of the package that wrote it, and whether a type implements a trait is the union over every ledger the program reads plus its own.[impl."Handler for Static"]
An impl may stand in the trait’s package, the type’s, or a consumer’s, so no
ledger can list implementors completely and none claims to. An impl no read
ledger wrote does not exist for this program.
There is no dyn, so at every instantiation the concrete type is known: its own
sync and throws are what that call has, and the trait’s declaration is the
ceiling an impl may not exceed, checked where the impl is written
(NK1129, NK1130). The body is checked against the trait’s entry (D8), which
is the right answer for its signature.
The claim Dog answers for Speaks is made in one place, the impl head, and
it is recorded in the ledger rather than found by the checker’s walk, because an
impl may stand in another file from the fn that asks. Absorbing a module
records it under the package’s spelling and the qualified one; an extra
spelling can only make the check fail open. Across a package it travels as
D13’s impl table.
NK1164).At the call the binding of D2 is asked the second question, at the free-call and
the method-call site alike. Two sentences, because there are two readers: a
concrete type is told write impl Speaks for Rock { … }, or pass a type that
already has one; a parameter of the caller’s own, which no impl can be written
for, is told add it to the bound: [V: … + Speaks] (Part III C.2).
.nika file wrote (ADR-023 D3), or one no ledger in reach
describes. A trait read from a dependency’s ledger is checked.signature, where the declaration writes it.[fn."dispatch"]
pub = true
signature = "[H: Handler](h: H) -> String"
The type parameters are declared in brackets before the parameters, as the
source declares them — [T](x: T) -> T for one with no bound — with several
traits +-joined and several parameters ,-separated. One string and not a
second key: the signature already carries the parameter, and the bound is the
rest of that sentence. A signature with no brackets parses as it always has. The ledger’s hash covers it, so adding or removing a bound
changes the file a consumer compiles against (ADR-100).
A package writes its own names bare and a consumer writes the path (ADR-286 D1), so the trait name is qualified on the way in: every prefix of the entry’s own key is tried, and the first spelling this program’s trait map knows wins. Only a spelling the program has; where none matches, the name stands as written and D17 applies. It is D15’s under both spellings, asked from the reading side.
User: Ada.say on its T
is a body whose callers have all been asked, in one file, across files, and
across a package (NK1164 naming handler::Handler and offering
impl handler::Handler for Bare { … }).let wrong: String = hand(n) is
NK1103 naming i64.trait table, an impl table and a bound inside the
signature (Part III 13.5). A package can publish a trait and a consumer can
constrain by it; a library author writes nothing new.impl’s own parameter. impl Stack[T] has nowhere for a
: Summarize to stand (D4).[T: A + B] takes whichever
bound declares it first; Rust calls the call ambiguous.where clause. None has a spelling.enum Maybe[T]), variance, and lifetimes on
parameters.hand(7) is ?, because a literal carries no type
on purpose; ADR-285’s question.impl A for T — is not decided
here.[T: Struct] (ADR-304 D2) is
answered by a declaration and records the same way.fn entries is
ADR-304’s ground.