Comptime and reflection
Goal: run logic at compile time, parametrize types (generics), inspect types with
reflect(T), and lock down contracts withassert/assume/@requires.
Comptime: the pure core running in the compiler
Section titled “Comptime: the pure core running in the compiler”comptime is the “evaluate this at compile time” modifier. It applies to a binding, a block, a loop,
a match, and a fn:
const SIZE := 64 // const = comptime letcomptime { assert (SIZE <= 1024) // checked at build time}type is a compile-time value: types are first-class values. A generic is just a
function that takes/returns type, executed by the compiler.
Generics: [...] is compile time, (...) is runtime
Section titled “Generics: [...] is compile time, (...) is runtime”decl List[T] { items: [*]T; len: usize; cap: usize } // sugar for a comptime fn that returns type
fn repeat[n: int](s: string) -> string // n: compile time; s: runtimefn zeros[N: usize]() -> [N]int // N sizes the array at compile timeEach distinct value in [...] generates a specialized function, that is, monomorphization.
Constraints are declared, with a strict symbol convention: + means implements an interface,
: is a whitelist (belongs to a set):
fn serialize[T + Serializable](v: T) -> []byte // capabilityfn encode[T: {i8, u16, bool}](v: T) -> []byte // whitelistfn proc[T: {i8, u16} + Serializable](v: T) -> []byte // combinedThe error blows up at the instantiation boundary, clean, and not three layers deep like in Zig’s duck typing.
Dispatch: monomorphization vs vtable
Section titled “Dispatch: monomorphization vs vtable”Two forms, choose by need:
T + Trait/ HKT generates monomorphization (static; necessary when the return depends onT, e.g.map[A,B](xs: C[A]) -> C[B]).- an interface value generates a vtable (erased), chapter 11.
Reflection: reflect(T) (the compiler package)
Section titled “Reflection: reflect(T) (the compiler package)”reflect(T) exposes a type’s structure to comptime code: it is what auto-derives
serialize/hash without you writing anything. The shape of the type is a variant-set, matched with an exhaustive comptime match:
use compiler.{reflect, fail}
fn serialize[T](v: T, out: mut Buffer) { comptime match reflect(T).kind { Struct(s) => comptime loop f in s.fields { serialize(f.get(v), out) } // f.get reads the field Tuple(parts) => comptime loop p in parts { serialize(p.get(v), out) } // positional Enum(e) => { put_varint(out, e.tag(v)); /* + active variant's payload */ } Slice(_) => { put_varint(out, v.len); loop x in v { serialize(x, out) } } // length-prefix + N Array(_, _) => loop x in v { serialize(x, out) } // n comes from the type Optional(_) => { out.write_byte(v.is_present() as byte); if v.is_present() { serialize(v.or_panic(), out) } } String => { put_varint(out, v.bytes.len); out.write(v.bytes) } // count + UTF-8 bytes Int(_) | Float(_) | Bool => out.write(v.to_bytes()) Ptr(_) | ManyPtr(_) | Channel(_) | Function(_) | RawPtr => fail("does not serialize: an address does not cross the boundary") // fail, from compiler _ => fail("non-serializable kind") // no value falls here, all 10 kinds above }}In practice you never see reflect: you write [T + Serializable] and the derivation runs underneath.
Whoever drops down to reflect(T) is a lib author (encoding/json/hash).
Contracts: assert / assume / @requires
Section titled “Contracts: assert / assume / @requires”The containment of danger (chapter 13) has two levels. A boundary contract (@requires /
postcondition) makes a function safe to call: the compiler checks at every call (static
where it can prove, panic where it can’t):
@requires(i < buf.len)fn at(buf: Buffer, i: usize) -> u8 == buf.bytes[i] // terse postcondition in the return type{ return unsafe raw_read(buf.ptr + i) assume "valid ptr: Buffer invariant + the @requires bounds" }Knocking down an operation in the body: assert (cond) checks (panic if false); assume "reason" trusts
(no check, no license for UB). Two keywords, one intent each.
Next: 11 · Interfaces