Compile-Time Metaprogramming
comptime is what replaces the entire preprocessor and macro machinery of C: ordinary Zig code runs while the compiler is compiling. Types become values, values become types, and whole programs can be specialized per call site — with the same syntax and the same safety checks.
The Compile-Time Concept
Every value in Zig has a moment when it becomes known: when the compiler runs, or when the program runs. Zig lets you write code that executes in the first phase. Anything marked or inferred as compile-time-known can be folded, checked, and specialized before a single instruction ships.
What Runs at Compile Time
- Top-level
constdeclarations are evaluated at compile time. - Values like
comptime_intand array lengths exist only in the compiler. - Any expression forced into a compile-time context runs entirely in the compiler.
const size = 4 * 1024; // computed by the compiler, never at runtime
const page: [size]u8 = undefined; // array length is a compile-time value
The comptime Keyword
comptime creates a scope guaranteed to run in the compiler. It is the legal, visible, typed replacement for #define and template mechanics.
comptime {
if (std.builtin.cpu.arch != .x86_64) {
@compileError("this demo targets x86_64 only");
}
}
Compile-Time Parameters
A parameter declared comptime must be known when the function is compiled. The classic case is a type parameter — that is how Zig does generics without a separate template language.
comptime T: type
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// max(u32, 3, 9) instantiates a specialized u32 version at compile time
const bigger = max(u32, 3, 9);
Generic Data Structures
The same trick builds containers. The standard library is full of these: std.ArrayList(T), std.AutoHashMap(K, V). Each instantiation is type-safe and monomorphized — no boxing, no virtual dispatch.
fn Stack(comptime T: type) type {
return struct {
items: []T = &.{},
fn push(self: *@This(), value: T) void {
std.debug.print("push {any}\n", .{value});
}
};
}
var int_stack = Stack(u32){};
int_stack.push(7);
Type Reflection
Because types are values, you can inspect them at compile time and generate code from what you find. This is reflection in the same language, not a parallel scripting system.
@TypeOf and @typeInfo
const t = @TypeOf(42); // comptime_int
const info = @typeInfo(MyStruct); // a tagged union describing the struct
// std.meta.fields(MyStruct) lists the fields for inspection
std.meta in Practice
A canonical use: verifying a change did not break a type contract. Reflection turns a promise into a compile-time guarantee checkable in a test.
test "struct has an id field" {
const fields = std.meta.fields(Config);
try std.testing.expect(fields[0].name == "id");
}
Compile-Time Expressions
Any ordinary expression can be forced into a compile-time context, and the compiler will either compute it or refuse loudly. This is how Zig keeps metaprogramming honest.
comptime Blocks
comptime {
const n = 1 + 1;
if (n != 2) @compileError("arithmetic is broken");
}
@compileError
Custom diagnostics read like normal compile errors, appear at the exact definition site, and can encode entire validation rules — the modern, safe version of preprocessor assertions.
fn ensure(comptime cond: bool) void {
if (!cond) @compileError("requires condition to hold");
}
ensure(1 < 2); // evaluated at compile time
@embedFile
Files can be baked into the binary at compile time: const shader = @embedFile("shader.wgsl");. No runtime reads, no packaging bugs — the bytes are part of the executable.
Figure 1 — source becomes specialized machine code: comptime stages vanish before runtime.
Common Pitfalls
Evaluation Branch Quota
Comptime evaluation is bounded to keep compiles fast; hitting error: evaluation exceeded 1000 back branches usually means restructuring — often by moving a loop into a comptime function the optimizer can reduce.
Treating Runtime Values as Compile-Time
Passing a runtime variable into a comptime context is a compile error. The error is the training: ask whether the value could be a constant — constants give the compiler the freedom it needs.
Next: Pointers & Slices — explicit memory access in Zig.