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 const declarations are evaluated at compile time.
  • Values like comptime_int and 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.

comptime versus runtime stages

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.

Thinking in Zig: there is no macro language to learn and no generated source to debug. comptime code is Zig, debuggable with the same errors and the same tests — one compiler, two phases.

Next: Pointers & Slices — explicit memory access in Zig.