ADR-330: A Type Is Passed Where a Function Asks for One, Answers Its Kind as a meta::Type That Carries Types, and a match on the Kind Checks Each Arm Under the Kind It Names

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

1. Question

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.

2. How others do it

  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.

3. Options, weighed

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

4. Decision

D1 — A type parameter written in the parameter list is filled by the type the call writes there.

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.

D2 — A type is the handle to everything about it: 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.

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

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

D5 — A reflected field’s .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).

D6 — A 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.

D7 — A grammar names no type of the program: 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).

D8 — How a value is reached is 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.

D9 — A call may name a function’s type arguments in brackets, 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.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.