Comptime and reflection
The redefinition: metaprogramming is a library, not magic. Comptime is pure code running in the compiler; reflection has no builtin at all.
The idea
Section titled “The idea”Generating code from types (serialize, hash, derive) needs some form of
metaprogramming. The historical choices: macros (C, text substitution), procedural macros
(Rust, a second language), or a handful of builtins (@sizeOf, @typeName, in Zig). Each one
adds a way of thinking separate from the language.
How Makoto redefines it
Section titled “How Makoto redefines it”Comptime (Zig style) is simply “evaluate this at compile time”: it applies to a binding, a block,
a loop, a match and an fn. type is a first-class value: a generic is a function that receives and
returns type, executed by the compiler. There is no separate language: it is the same code, running
earlier.
Reflection has zero builtins. There is no @type_name/@size_of in scope. Everything lives in the
compiler package: reflect(T) (the structure of the type, matched with comptime match reflect(T).kind), fail(msg)
(aborts compilation with a message). That is how serialize/hash/json auto-derive, without you
writing anything, and even so none of that is a builtin.
And the comptime match reflect(T).kind is exhaustive: the compiler demands that you handle all
the kinds (or a _/fail). A non-serializable type does not compile: the arm Ptr | ManyPtr | Channel | Function | RawPtr => fail(...) refuses it to its face, instead of generating garbage.
The reasoning
Section titled “The reasoning”The root rule is non-poisoning taken to the limit: the global scope carries no function at all.
Each builtin would be a poisoner of the namespace: it conflicts with user names, pollutes the global, and
makes you memorize “is it @size_of or size_of?”. Moving everything to reflect(...)/fail(...) keeps the namespace
clean: metaprogramming is a library you import, not embedded magic.
Why is comptime safe to exist? Because what runs at compile time is almost forced by the
architecture to be the pure and deterministic core of the language, without IO, without processes, without
channels, without syscalls. Comptime never reads a runtime value. That is what lets the compiler
optimize aggressively and guarantees reproducibility, and it is why the transient scope exists
(mut/lambdas are transient expressions, tied to their place, without leaking from comptime to runtime).
The exhaustiveness of match reflect.kind is not a detail: it is the application of the same principle as the closed-domain
match (ch. 07) to derivation. And it is a lesson learned the hard way: the absence of an arm (Enum
missing, then String missing) bit the design more than once, and the rule “every match reflect.kind covers the entire set” became a permanent check.
Why not the alternatives
Section titled “Why not the alternatives”- Macros (C). Text substitution: it breaks lexing and structure, and you debug the macro as a separate language. Comptime is real semantic execution, not text.
- Scattered builtins (
@sizeOf/@typeName, Zig). Each one pollutes the global and becomes “which builtin is which?”. Everything underreflect(...)is a namespace, not a vocabulary to memorize. - Implicit traits/derives (Rust). Without an exhaustive
match, you discover that a type “almost” satisfies three layers of compilation below, with a confusing error. Derivation byreflect+ exhaustive match gives the error at the boundary, clear. - Letting comptime values leak into runtime. It would break determinism and blind the optimizations; the transient scope prevents it.
The concrete pain
Section titled “The concrete pain”The C macro that did not expand right because of a parenthesis and you debug blindly. The new Zig programmer stuck on “what is the reflection function again?”, because there are twenty builtins. The Rust derive that fails deep in the instantiation with a message that does not point to your code. Each one is metaprogramming that became a separate way of thinking, with its own rules and its own surprises.
The mental model
Section titled “The mental model”Metaprogramming is a library, not magic. Comptime is your code running in the compiler, pure and deterministic;
reflect(T)asks “what type is this?” with no builtin; thematchover the kind is exhaustive, so the compiler forces you to handle everything.
The accepted friction
Section titled “The accepted friction”Zero builtins costs verbosity: you always write reflect(T).kind instead of a short @kind. And
comptime being pure (no IO, no reading runtime) limits what it can do, in exchange for total
reproducibility. The bet is the usual one: a bit more text, in the name of a clean namespace and of a sharp
boundary between what runs at build and what runs in the program.
Next: 09 · Strings