Status: Superseded by ADR-295 — its D1-D5 are D1-D5 there Date: September 14, 2026 Target Version: Nikaia 0.0.8 Related: ADR-024 D4 (which erased a generic parameter, and why that was right at the time), ADR-031 (a signature’s variables bind from the receiver — the machinery this points at a new place), ADR-011 D2 (the emitter resolves nothing, so what it does not write is not written)
Part I 4.6 writes struct Box[T], and the language had the form without having
the feature. The [T] was read by the parser, erased by the checker and
dropped by the emitter, which wrote {vis}{pausing}fn {name}({params}){ret}
and had no slot for one. So this compiler accepted
fn hand[T](x: T) -> T { return x }
and rustc answered “cannot find type T in this scope” about a file nobody
wrote, with “you might be missing a type parameter” as the help — which is
Part III C.1’s class exactly.
And writing the <T> does not close it, which is worth stating because it is
the obvious move. Measured:
fn hand<T>(x: T) -> T { x } // compiles
fn shout<T>(x: T) -> String { x.to_uppercase() } // "no method named
// `to_uppercase` found for
// type parameter `T`"
The second needs a bound, and ADR-024 D4’s erasure meant the
checker could not ask for one: T became ?, ? fits everything, so the body
type-checked clean and failed below. Emitting the parameter alone turns “every
generic fails” into “every generic whose body uses its parameter fails” — the
same C.1 class one size smaller.
Why D4 erased, and why that is not reversed here. D4’s reason was a false
positive: a T that survives into a comparison makes the checker report that
i64 is not T, which is a lie told to a caller who passed a perfectly good
i64. That reason is about one of the two places a parameter is read, and
the erasure was applied to both. Separating them is the whole of this record.
Two readings of one name, and the split is exact:
T is a type and fits compares it. The body did not pick
T, its caller did, so a body that puts an i64 where its caller’s T is
wanted is wrong and saying so is not a false positive — it is the only true
thing to say. This is what gives D5 something to refuse.T is a Ty::Var: bound from what the caller passes and
substituted away, or ? where nothing bound it. It never survives into a
comparison, so no caller is ever told that i64 is not T. D4’s reason is
preserved exactly where D4’s reason applies.Self keeps the old treatment and is erased. It is a name for the type an
impl is on, no call site binds it, and a comparison against it could therefore
only be the false positive D4 named.
ADR-031 bound from the receiver and said so on purpose: binding from arguments is where a signature language grows into a unification algorithm. This takes that step and keeps it one step.
let n: i64 = 7
let it = hand(n) // `it` is an `i64`
A free function has no receiver, so before this the binding had nothing to work
from and hand(n) was ?. The same ty::bind, pointed at one pattern per
argument, answers i64. A generic struct’s literal is the same question with
the fields as the arguments: Pair { first: n, second: n } is a Pair[i64],
and a value’s own type arguments bind its declaration’s parameters positionally
when a field is read back off it.
It stays one structural walk. No queue, no fixpoint, no occurs check — and
that is a property of the design rather than of the implementation: a type
parameter is written (fn hand[T]), never inferred, so there is never a
variable on the right-hand side to solve for. §5 measures what that costs.
A mismatch binds nothing, as the receiver’s always did. The question is what a
caller can tell the signature, and what it could not tell becomes ?.
fn, on a struct, and on the impl over it.fn hand[T](x: T) is fn hand<T>(x: T); struct Pair[T] is struct Pair<T>;
impl Pair[T] is impl<T> Pair<T>. Where a type also carries the input lifetime
it stands first, which is the order Rust wants: struct Row<'a, T>.
impl’s parameters come from the slots it already writes, and there is no impl[T] spelling.impl Stack[T] declares T; impl Stack[i64] declares nothing and applies
i64. What tells them apart is whether the slot names a type: a bare name
that is neither one of Part I 2.2’s types nor one this file declares stands for
a type rather than being one.
A second list — impl[T] Stack[T] — would be a spelling Part I does not have,
bought to avoid a rule that reads the list already written. And the rule lives
in one function that the ledger and the emitter both call, because two
copies of it would be one silent disagreement about which names a method’s
signature was recorded with.
NK1126: a member reached on a parameter that has no bound.error[NK1126]: `T` stands for a type the caller picks, and nothing says it has
a method `to_uppercase`
This is the refusal that makes D3 worth anything. Without it, a body that uses
its parameter still fails below, and the message is still rustc’s about a file
nobody wrote. Fields and methods both, because both are members and both leak
the same way.
It says “no bound” rather than “no such method” on purpose: the method may well exist on every type a caller will ever pass, and what is missing is the sentence that says so.
Part I 4.6’s own example compiles, and so does the impl over it. That is
the second chapter-4 program that was written down before it was possible.
A generic call is typed rather than opaque. let wrong: String = hand(n)
where n is an i64 is NK1103 naming i64, and so is the same mistake read
back off a generic struct’s field. Before this both were ? and neither was a
question this compiler could answer.
The ledger’s signature column records $T where it recorded ?.
Part III 30’s row said a generic parameter is written ?; it is now written as a
variable, which is what lets a reader of the ledger see that hand’s result is
its argument. Part III 30 is corrected with this.
What it costs the compiler is nothing measurable — §5.
[T: Summarize] has no syntax and nothing to name: Part I 4.7’s
trait Summarize { … } is a parse error, so a trait can be implemented and
not declared. D5 refuses the programs that would need one and names what is
missing, which is the honest state until the trait declaration exists. This is
the next step and it is a different record.enum Maybe[T] is not read here; the struct is what
Part I 4.6 writes.hand(7) is still ?, because a literal carries no
type on purpose (Part I 2.4) and a variable bound to ? is ?. Nothing is
wrong with the program — it compiles and rustc infers what Nikaia did not —
but the checker has less to say about it than about hand(n). Whether a
literal’s type should come from where it stands is ADR-060’s
question and not this one.where clauses. None of the three
has a spelling in this language, and none is needed by anything that can be
written today.rustc, and hereThe question was asked before the work: what does building generics properly cost in performance? It has three readings and they have three different answers, all measured rather than estimated.
The generated program: nothing. Rust monomorphises, so a lowered generic is the same machine code as the concrete function somebody would have written instead. Four million calls each, best of three:
| time | |
|---|---|
| through a generic | 137.5 ms |
| through its concrete twins | 139.5 ms |
The difference is ordering noise in both directions across runs. There is no dispatch, no boxing and no indirection to pay for.
rustc: what the copies cost, and generics are the cheaper way to ask for
them. Sixty functions used at eight types each, as one generic and as the same
thing written out, against the un-multiplied sixty:
rustc -O --emit=obj |
object | |
|---|---|---|
| 60 generic functions, 8 instantiations each | 273 ms | 121.4 KB |
| the same 480 written out by hand | 304 ms | 119.2 KB |
| 60 plain functions, no multiplication | 121 ms | 20.7 KB |
So the generic form is 11 % faster to compile than the duplication it replaces — one body to parse and type-check rather than 480 — and within 2 % on size. What costs 2.3× in time and 5.9× in code is having 480 bodies at all, and that is the author’s instantiation, not genericity’s overhead. The thing to watch is therefore instantiation count, which is a property of how a library is written; it is not a reason to avoid the feature.
This compiler: nothing measurable. The front end before and after, best of five, on the three largest examples:
| before | after | |
|---|---|---|
| fixed cost (start + ledger) | 4.72 ms | 4.53 ms |
examples/1brc.nika (264 lines) |
6.92 ms | 6.40 ms |
examples/json/src/main.nika (265 lines) |
9.08 ms | 9.13 ms |
examples/k-nucleotide/src/main.nika (202 lines) |
6.77 ms | 6.68 ms |
Unchanged within the noise, which is what D2’s last paragraph predicts: the work
added is one ty::bind per call against a signature that is already in hand, and
bind is a structural walk over two types. It is the same order of work the
receiver’s binding has done at every method call since ADR-031.
The figure that would change this is the one nobody should pay: inference.
A checker that worked out T from use rather than reading it off the
declaration is a constraint solver, and constraint solvers have pathological
inputs. D2’s “written, never inferred” is what keeps the third row of this table
flat, and it is a decision rather than a consequence.
Ty::parameterise beside Ty::erase, and the ledger records $T;
Checker::type_parameters and struct_parameters, so a body reads a parameter
as a type and a literal binds one; from_arguments beside bindings, used by
both the free call and the method call; contracts::impl_parameters, called by
the ledger, the checker and the emitter; the <…> written at all three
positions; NK1126 for a field and for a method. Exercised by nine programs in
crates/nikaia/tests/generics.rs, six of them compiled by rustc rather than
compared as text.