⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Comptime

comptime is Zig’s answer to templates, macros, generics, reflection, and constant folding, all with one mechanism: run normal Zig at compile time, where types are values.

const std = @import("std");
const expect = std.testing.expect;

fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// A generic function is just one that takes a `type` parameter.
fn List(comptime T: type) type {
    return struct {
        items: []T,

        pub fn first(self: @This()) ?T {
            return if (self.items.len == 0) null else self.items[0];
        }
    };
}

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

test "evaluate at compile time" {
    // `comptime` forces evaluation during compilation; the binary contains
    // the answer, not the recursion.
    const small = comptime fibonacci(10);
    try expect(small == 55);
    try expect(@TypeOf(small) == u32);
}

test "the branch quota is a real limit" {
    // Comptime evaluation is capped at 1000 backwards branches so a runaway
    // computation fails the build instead of hanging the compiler.
    // fibonacci(20) blows past that; raise the ceiling deliberately.
    @setEvalBranchQuota(100_000);
    const result = comptime fibonacci(20);
    try expect(result == 6765);
}

test "types are comptime values" {
    const T = u16;
    const value: T = 300;
    try expect(@TypeOf(value) == u16);
}

test "generic data structure" {
    var backing = [_]i32{ 10, 20 };
    const list = List(i32){ .items = &backing };
    try expect(list.first().? == 10);

    // A different T produces a genuinely different type.
    try expect(List(i32) != List(u8));
}

test "generic function" {
    try expect(max(u8, 1, 2) == 2);
    try expect(max(f32, 1.5, 0.5) == 1.5);
}

test "comptime blocks can assert" {
    comptime {
        // A failed comptime assert is a compile error, not a test failure.
        std.debug.assert(@sizeOf(u32) == 4);
    }
}

Types are values

A type is an ordinary comptime value. That single fact removes the need for a separate generics language:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

comptime T: type says this parameter must be known at compile time. Each distinct T produces a separately compiled function: monomorphisation, with no special syntax.

Generic types are functions returning types

fn List(comptime T: type) type {
    return struct { items: []T, ... };
}

List(i32) and List(u8) are genuinely different types. std.ArrayList and std.HashMap are ordinary functions like this one. Nothing in the standard library gets privileged syntax you cannot use yourself.

The branch quota

Comptime evaluation is capped at 1000 backwards branches, so an accidental infinite loop fails the build instead of hanging the compiler. Real work often exceeds it:

@setEvalBranchQuota(100_000);
const result = comptime fibonacci(20);

Raising it is normal; needing to raise it to something absurd is a hint that the work belongs at runtime.

Where it pays off

Anything computed at comptime is not in the binary as code, only as its result. Format strings are checked against their arguments, lookup tables are generated, and dead configuration branches disappear entirely.