Skip to main content

Comptime Basics

Zig evaluates code at compile-time using the comptime keyword. This enables powerful metaprogramming without macros or templates.

Comptime Variables

const builtin = @import("builtin");
const separator = if (builtin.os.tag == .windows) '\\' else '/';
This expression is evaluated at compile-time, creating a platform-specific constant.
Comptime expressions must be fully evaluable at compile-time - they cannot depend on runtime values.

Comptime Parameters

Functions can accept compile-time parameters with the comptime keyword:
fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

// The generic List data structure can be instantiated by passing in a type:
var buffer: [10]i32 = undefined;
var list = List(i32){
    .items = &buffer,
    .len = 0,
};
Functions that take type parameters are generic functions evaluated at compile-time.

Generic Data Structures

Generic types are created by returning structs from functions:
fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last: ?*Node,
        len: usize,
    };
}

test "linked list" {
    // Functions called at compile-time are memoized.
    try expect(LinkedList(i32) == LinkedList(i32));

    const ListOfInts = LinkedList(i32);
    var node = ListOfInts.Node{
        .prev = null,
        .next = null,
        .data = 1234,
    };
}
Generic functions are memoized - calling List(i32) multiple times returns the same type.

Comptime Evaluation

Compile-time code can perform complex computations:
const expect = @import("std").testing.expect;

const CmdFn = struct {
    name: []const u8,
    func: fn (i32) i32,
};

const cmd_fns = [_]CmdFn{
    CmdFn{ .name = "one", .func = one },
    CmdFn{ .name = "two", .func = two },
    CmdFn{ .name = "three", .func = three },
};

fn one(value: i32) i32 {
    return value + 1;
}
fn two(value: i32) i32 {
    return value + 2;
}
fn three(value: i32) i32 {
    return value + 3;
}

fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
    var result: i32 = start_value;
    comptime var i = 0;
    inline while (i < cmd_fns.len) : (i += 1) {
        if (cmd_fns[i].name[0] == prefix_char) {
            result = cmd_fns[i].func(result);
        }
    }
    return result;
}

test "perform fn" {
    try expect(performFn('t', 1) == 6);
    try expect(performFn('o', 0) == 1);
    try expect(performFn('w', 99) == 99);
}

Inline Loops

Loops can be unrolled at compile-time with inline:
test "inline for" {
    const types = [_]type{ i8, i16, i32, i64 };
    var sum: usize = 0;
    inline for (types) |T| {
        sum += @sizeOf(T);
    }
    try expect(sum == 1 + 2 + 4 + 8);
}
inline for (items) |item| {
    // Loop is unrolled at compile-time
}

Comptime Blocks

You can explicitly mark blocks as compile-time:
test "comptime blocks" {
    comptime {
        var x: i32 = 1;
        x += 1;
        try expect(x == 2);
    }
}

Type Reflection

Zig provides built-in functions for type introspection:
test "type reflection" {
    const T = @TypeOf(10);
    try expect(T == comptime_int);
    
    const info = @typeInfo(i32);
    try expect(info == .int);
}

Common Type Functions

const x = 10;
const T = @TypeOf(x); // comptime_int

Comptime Assertions

You can assert conditions at compile-time:
const assert = @import("std").debug.assert;

fn fibonacci(comptime n: u32) u32 {
    comptime assert(n < 47); // Prevent overflow
    
    if (n < 2) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

test "fibonacci" {
    try expect(fibonacci(10) == 55);
}
Excessive compile-time computation can slow down compilation. Use it judiciously.

Generic Functions

Functions can be generic over types:
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

test "generic max" {
    try expect(max(i32, 10, 20) == 20);
    try expect(max(f64, 3.14, 2.71) == 3.14);
}

Duck Typing

Compile-time duck typing allows generic code without explicit interfaces:
fn print(comptime T: type, value: T) void {
    // If T has a print() method, call it
    if (@hasDecl(T, "print")) {
        value.print();
    } else {
        std.debug.print("{any}\n", .{value});
    }
}

Comptime String Manipulation

fn concat(comptime a: []const u8, comptime b: []const u8) []const u8 {
    return a ++ b;
}

const greeting = concat("Hello, ", "World!");

Container-Level Comptime

Code at container level is implicitly compile-time:
const os_msg = switch (builtin.target.os.tag) {
    .linux => "we found a linux user",
    else => "not a linux user",
};
All container-level declarations are evaluated at compile-time in dependency order.

Comptime Type Manipulation

fn PointerTo(comptime T: type) type {
    return *T;
}

fn SliceOf(comptime T: type) type {
    return []T;
}

test "type manipulation" {
    const IntPtr = PointerTo(i32);
    try expect(IntPtr == *i32);
    
    const IntSlice = SliceOf(i32);
    try expect(IntSlice == []i32);
}

Inline Switch

Switch statements can be inlined for comptime values:
fn dispatch(comptime tag: enum { add, sub, mul }) i32 {
    return inline switch (tag) {
        .add => 1,
        .sub => 2,
        .mul => 3,
    };
}

Best Practices

  • Use comptime for type parameters and generic programming
  • Leverage compile-time evaluation for zero-cost abstractions
  • Use inline loops when you need unrolling
  • Prefer compile-time assertions to catch errors early
  • Use type reflection for flexible generic code
  • Remember that comptime functions are memoized
Comptime is one of Zig’s most powerful features - it replaces macros, templates, and generics from other languages with a single, unified mechanism.

Build docs developers (and LLMs) love