Lesson 13 / 25

comptime Basics

Run code at compile time and use compile-time parameters.

One language for run time and compile time

Zig's signature feature is comptime: ordinary Zig code can run during compilation. A comptime block or expression is evaluated by the compiler, and its result is embedded in the program: lookup tables, precomputed constants or validated configuration cost nothing at run time. Function parameters marked comptime must be known at compile time, which enables specialisation: the function is instantiated separately for each distinct value. Types are values at compile time (of type type), so they can be passed to functions, stored in constants and returned, which is how Zig implements generics without a separate template language. @compileError reports custom compile errors, perfect for validating assumptions ("this struct must be 64 bytes"). inline for and inline while unroll loops at compile time, for example over a struct's fields. There are limits: comptime code cannot perform I/O or call external functions, and long comptime evaluations need @setEvalBranchQuota. This replaces C's preprocessor macros, C++ templates and many code generators with one consistent language.

Compile time and run time

comptime code runs in the compiler; its results become constants in the binary.

A compiler box with a gear running a small function and producing a table, which is then baked into a binary file shown on the right.
Figure 5.1 — Computation moved to compile time.

Lookup tables and assertions at compile time

A table computed by the compiler and a size check.

const std = @import("std");

fn fib(n: u64) u64 {
    return if (n < 2) n else fib(n - 1) + fib(n - 2);
}

// Computed entirely by the compiler; the binary just contains the array
const fib_table = blk: {
    @setEvalBranchQuota(100_000);                 // naive recursion needs many comptime steps
    var table: [20]u64 = undefined;
    for (&table, 0..) |*slot, i| slot.* = fib(i);
    break :blk table;
};

const Header = extern struct {
    magic: u32,
    version: u16,
    flags: u16,
    length: u64,
};

comptime {
    if (@sizeOf(Header) != 16) @compileError("Header must be exactly 16 bytes for the file format");
}

fn repeat(comptime n: usize, ch: u8) [n]u8 {          // n is part of the return type
    var out: [n]u8 = undefined;
    @memset(&out, ch);
    return out;
}

pub fn main() void {
    std.debug.print("fib(19) = {d}\n", .{fib_table[19]});   // 4181, no run-time computation
    const line = repeat(10, '=');
    std.debug.print("{s}\n", .{&line});
    std.debug.print("Header is {d} bytes\n", .{@sizeOf(Header)});
}

Compile-time checks are documentation

A comptime assertion about a struct size, an enum count or a configuration value documents an assumption and turns its violation into a build failure instead of a production bug.

Quick check: What can a function do with a `comptime T: type` parameter?

  • Use T as a type in its body, making the function generic over T
  • Nothing; types cannot be parameters
  • Change T at run time
  • Only print T
Answer

Use T as a type in its body, making the function generic over T — Types are compile-time values, which is how Zig does generics.