meta::Type That Carries Types, and a match on the Kind Checks Each Arm Under the Kind It NamesStatus: Accepted
Date: October 7, 2026
Answers: how a program describes a type it holds no value of - an API
description, a schema, a form - down through the types its fields have
Related:
ADR-304 D4, D6 (reflection, unrolled per type);
ADR-329 D1 (.ty);
ADR-323 D1 (meta::Type);
ADR-251 (the ledger’s constant);
Part I 4.6, Part II 10.3, 10.5
How does a function describe a type - its fields, and the types of those fields, down to the end - when no value of it exists?
A type parameter is filled from the arguments (Part I 4.6), and a shape walk
reads its type off its first argument (ADR-304 D6). An API
description is made before any request has come, so there is no value to read
a type from. And a field’s type, .ty, was a description - a struct as a
Record of its fields (ADR-329 D1) - which carries what was
copied into it and nothing that is added to a type later: its documentation,
its name, what #496
decides.
| A type without a value | Walking a type’s parts | |
|---|---|---|
| Go (before generics) | reflect.TypeOf((*T)(nil)).Elem(), at run time |
reflect.Type, at run time |
| Go, Rust, Kotlin | f[T](), f::<T>(), f<T>() |
Rust: a trait impl per type, generated by derive |
| Zig | f(T): a type is a build-time value |
@typeInfo(T), a tagged union; inline switch checks only the arm taken |
| C++ | f<T>() |
if constexpr checks only the branch taken |
| Swift | f(T.self), a metatype value |
Mirror, at run time, needs a value |
From memory, not checked against a version. What transfers: Zig’s type as an argument and its kind as a tagged value. What does not: an arm left unchecked because no call took it.
| Option | Why it lost |
|---|---|
f[T]() as the way to describe a type |
a second form at every call; schema(T) says it where the parameter list holds the type. f[T]() is allowed beside it (D9) |
fn f(T: type) |
the bound (Struct) has no place, or needs a form of its own |
fn f(comptime T: Struct) |
comptime marked bindings (Part II 10.2), not parameters. Since ADR-333 D3 a value parameter may be comptime; a type is still passed as D1 says. |
The kind with its details inside (Record(fields)) |
a type that holds itself has no end; every detail added later changes the enum and every match on it |
A handle of its own (Info) beside the type |
two ways to the same facts, T::fields and info.fields |
| Check only the arm the kind takes | an arm no call reaches is never checked; a mistake in it waits for the first such type |
A trait impl per kind |
the walk is spread over many impls instead of written once |
Named("Mode") kept for a grammar |
a type of the program as text, which nothing used |
Ref(A), Shared(A), SharedMut(A) as kinds |
how a value is reached is not the form of its data; every describer would write an arm that only passes A on |
A kind per std type (Duration, Path) |
the enum grows with std, and a crate’s types still have none |
Changes ADR-304 D6: the type may come from the call, not only from the first argument.
fn schema[T](T) -> String { … }
schema(CreateUser)
The bound stays in the brackets, fn schema[T: Struct](T). The argument is a
type the call names, or one the body holds: a parameter, field.ty, a type a
match arm binds (D4). A value there, or a type where a value belongs, is
NK1230. Nothing of the argument reaches the running program; the function
is emitted once per type, as a shape walk is.
T::kind, T::access, T::fields, T::variants, T::doc, T::name.T::kind is its kind (D3); T::access how it is reached (D8); T::fields and T::variants as before
(ADR-304); T::doc the /// lines before its declaration,
empty as a field’s is (ADR-329 D2); T::name its name as the
declaration writes it. What is added later - aliases, full names, what
#496 decides - is a
member more, and nothing else changes.
T::kind is a meta::Type: the kind, carrying the types it is made of.Changes ADR-323 D1: the parts are types, not descriptions.
T |
T::kind |
|---|---|
i64, i32, u8, u64, u32, f64, bool |
I64, I32, U8, U64, U32, F64, Bool |
String, Bytes |
Text, Bytes |
scalar |
Scalar |
A? |
Maybe(A) |
Vec[A] |
List(A) |
HashMap[K, V] |
Map(K, V) |
Array[A, N] |
Array(A, N), N a number |
(A, B, …) |
Tuple(parts), the types in order |
a struct |
Struct(T) |
an enum |
Enum(T) |
| a type whose inside is not the program’s to see | Opaque(T) |
A type of std has the kind of what it is, wherever Nikaia can say it: a
struct of std is Struct. It is Opaque only where std reserves the right
to change its inside. A type of a Rust crate and a C opaque type are
Opaque. T::name tells one Opaque from another. A tuple’s parts is walked
as T::fields is, unrolled. ref A, Shared[A] and SharedMut[A] have the
kind of A, with A’s parts (D8). A function type has no kind yet; T::kind
of one is NK1232.
Changed by ADR-332 D1: a function type’s kind is Fn(F); NK1232 is retired.
match on a kind checks each arm under the kind its pattern names, and emits only the arm the type takes.fn schema[T](T) -> String {
match T::kind {
Text => "\"string\"",
List(it) => f"[{schema(it)}]",
Struct(s) => {
let mut out = "{"
for field in s::fields { out += f"\"{field.name}\": {schema(field.ty)}" }
out + "}"
}
}
}
In Struct(s), s is a type with the bound Struct, so s::fields is
reachable; in List(it), it is the element type. Every arm is checked, as a
match on an enum checks each arm with what its variant carries; no arm is
checked against a type it cannot be taken for. For each type the function is
emitted with the one arm that type takes.
.ty is the field’s type.Changes ADR-329 D1: not a meta::Type.
It is passed on as a type (schema(field.ty)), and asked as one
(field.ty::kind).
meta::Type exists only while the program is built.It carries types, which the running program does not hold. One that would
reach it - kept in a value of the program, or handed to code that runs then -
is NK1231. A grammar (ADR-323) builds a type with
Record(fields) and names the types its package can name. Out of the grammar
run a type crosses as its full name.
Named is gone.Changes ADR-323 D1, D3.
A grammar’s package cannot see the program that uses it, so Named("Mode") was
a name as text, resolved at the block. No grammar, example or test used it; a
row is mapped onto a type of the program from the program’s side, with
-> Contact (ADR-299 D15); and ending a type that holds itself,
its other use, is the type’s own (D2).
A type the caller hands to a grammar’s entry as a type argument is not such a name: the grammar receives it, as a generic function of a package does, and names nothing of the program (ADR-grammar-2 D1).
T::access, beside its kind.pub enum Access { Owned, Ref, Shared, SharedMut }
ref String, Shared[String] and String hold the same data; they differ in
how it is reached. T::kind of all three is Text, and T::access is Ref,
Shared and Owned. A function that describes reads the kind; one that builds
values asks the access, and one that builds an A where a ref A belongs is
refused at that unrolled turn, as any wrong type is. A? stays a kind: whether
a value may be absent is the form of the data.
f[T](); it is warned with NK1233, asking for its use case, unless a type argument fills a parameter no value parameter’s type names.Changed by ADR-grammar-2 D11: no warning where the type
is the call’s only source for it, as in Json::read[Contact](bytes).
Changes ADR-323 §3: type arguments at a call are not refused.
let n = id[i64](3) // warning[NK1233]: the argument already says T
let config = parse[Config](text) // no warning: nothing else says what T is
The types fill the function’s type parameters in order. It is a function’s form; a method’s is not decided. The warning names #514, where a use case is collected: a form that may be needed is allowed, and what it is needed for decides whether it stays, goes or narrows.
meta::Type is an enum that never reaches
run time; match on a kind is a narrowing the checker does for types as it
does for variants.Page[Address]), two
versions of one package in a build, types without a name - and nothing needs
one yet. T::name is the name the declaration writes, Page for
Page[Address].std types are Opaque: each type’s entry says it, by the rule of D3.