ADR-295: A Generic Is Checked at Both Ends — a Type Parameter Is a Type in Its Body and a Variable at the Call, a Bound Names a Declared Trait, and the Ledger Carries the Trait, Each impl and the Bound

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

1. Question

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.

2. How others do it

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.

3. Options, weighed

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.

4. Decision

D1 — Inside its own body a type parameter is a type; at a call site it is a variable.

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.

D2 — A variable binds from the arguments and from a struct literal’s fields, and is written, never inferred.

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.

D3 — The emitter writes the parameters, on a 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>.

D4 — An 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.

D5 — 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.

D6 — A trait declares signatures, and the syntax tree says so.

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.

D7 — A bound is written [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.

D8 — A bound is looked up in the ledger, under the trait’s name, before anything is refused, and it answers the whole call.

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.

D9 — A trait’s method is recorded with the words it was declared with.

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.

D10 — A bound takes a path, as every position that names a type does.

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.

D11 — use is unchanged: no name comes in, for a trait as for anything else.

D12 — The ledger has a 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.

D13 — Each 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.

D14 — A call through a bound resolves to the implementing type’s own entry.

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.

D15 — The ledger records who implements what, merged across units, under both spellings.

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.

D16 — A call whose type does not answer for the bound is refused here (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).

D17 — The refusal fails open where this compiler could not have the answer.

D18 — A bound reaches a caller inside the 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).

D19 — The bound arrives in the declaring package’s namespace and is read in the consumer’s.

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.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.