ADR-078: A Trait Is Declared, and a Bound Is Answered from the Declaration

Status: Superseded by ADR-295 — its D1-D4 are D6-D9 there Date: September 14, 2026 Target Version: Nikaia 0.0.8 Related: ADR-074 §4 (which named this as the prerequisite for a bound and left it open), ADR-031 (a signature binds from the receiver — a bound asks the same question of a different thing), ADR-055 D1 (async fn comes off the ledger’s sync column, which is what D4 is about), ADR-076 (which found trait as a name)

1. Context

Part I 4.7 writes

trait Summarize {
    fn summary(&self) -> String
}

as its own example, and it was a parse error at the keyword. A trait could be implemented and not declared: impl Summarize for User worked, so the traits a program could implement were the ones std and the Rust side already name.

ADR-074 §4 made it load-bearing: a bound names a trait, so [T: Summarize] had nothing to name, and generics stopped at the point where a generic body may move and pass its value and nothing else. NK1126 said exactly that to the user, which is honest and is not the same as finished.

2. Decisions

D1 — A trait declares signatures, and the shape says so.

Item::Trait holds TraitMethod, which has no body — not an Item::Fn with an empty block, and not one whose body became an Option. The difference between “declares this method” and “this method does nothing” is the whole of what a trait is, and a body that is absent cannot be accidentally emitted.

The emitter gets a second, smaller writer for the same reason. function reads the ledger for sync, for the Shared count of each position, and for whether the call can fail — every one of those is an answer about a body. A declaration has none, so it is written by something that says what a declaration is rather than by something that omits what a definition has.

D2 — A bound is written [T: Summarize], and several are [T: A + B].

Inside the brackets Part I 4.6 already puts the parameter, so the bound goes where a reader of that page looks for it, and it is a list because + is how two are written. <T: Summarize> and <T: Named + Aged> are what reach the generated file, in the order the source wrote them.

D3 — A bound is looked up in the ledger, under the trait’s own name, before anything is refused.

A trait’s methods are recorded as Summarize::summary — the same key shape an impl’s methods get, because a bound and a receiver ask one question: what does a value of this thing have. So when a member is reached on a type parameter, the bounds are tried as receivers and the call is asked again on the trait.

The consequence is the point: the bound answers the whole call and not merely whether it exists. Arity is NK1101 from the declaration, the argument types are NK1102, the result type is what a let or a return is checked against, and sync, throws and the binding of the signature’s variables all come from the same entry — exactly as they would from a receiver whose type was written down. NK1126 is unchanged where no bound declares the member, which is what keeps it honest: a bound is not a blanket licence.

Ledger::traits answers the question a bound asks first — is Summarize a trait at all — and a name that is not in it names none.

D4 — A trait’s method is recorded with the words it was declared with, and reads like a function type.

For a function the word says Asserted, its absence says No, and sync::infer then raises No to Inferred by reading the body (ADR-027). A declaration has no body, so what it says is what it is: sync where the word stands, and without it a method that may pause — the reading a function type has (ADR-102 D2, ADR-109 D1). sync::infer’s graph gets a trait’s methods as leaves with that answer, and a call through a bound pauses where the declaration allows it.

3. Consequences

Part I 4.7’s program runs, and prints User: Ada. With ADR-074 that is chapter 4’s generics section finished end to end: declare a trait, bound a parameter by it, call the method through the bound.

trait is a reserved word, which it was not — ADR-076’s sweep found it as a name, because let trait = 3 was a legal program that rustc refused about the generated file. It is the one word in the parser’s “nothing uses this yet” half that now has a construct, and it is there only because that is where the alternation’s width is safe.

NK1126’s help text is now reachable rather than terminal. It said “a bound that says which types T may be is not built yet”; the sentence stays true of what a bound cannot do and stops being true of bounds.

4. What this does not decide

5. What it cost to find

Three things went wrong in the order they had to. The bound parsed and emitted and the checker still refused the call, because the program’s ledger is absorbed from the unit’s and traits was the one map absorb_renaming left behind — visible only as a third resolution pass with an empty map where the first two had it. Then the call resolved and the lowering awaited a String, which is D4. Then the fixture itself did not compile, for a reason that has nothing to do with traits: Part I 4.7’s "User: " + self.username is &str + String, accepted here and refused below. That is a fourth finding and is in #207, not fixed here.

6. What is built

Item::Trait and ast::TraitMethod; trait reserved and the item in the grammar; GenericParam::bounds with [T: A + B]; Emitter::trait_method and Emitter::bounded, so a fn, a struct and a trait’s own methods all write their parameters with their bounds; contracts::trait_method and Ledger::traits, carried through absorb_renaming; Checker::bound_that_answers, asked before NK1126; and trait methods as unblocked leaves in sync::infer’s graph. Exercised by nine programs in crates/nikaia/tests/traits.rs, one of them run to see that it prints User: Ada.

This site is open source. Improve this page.