Comptime
Zig's comptime keyword lets you run code at compile time. This replaces macros and generics found in other languages with a single, unified mechanism that uses ordinary Zig code.
Compile-Time Values
Mark a variable or parameter as comptime to force evaluation at compile time.
const std = @import("std");
fn fibonacci(comptime n: u32) u64 {
// Naive recursion revisits the same values many times. The compiler caps
// comptime evaluation at 1000 backward branches by default to catch runaway
// loops; @setEvalBranchQuota raises that budget so this can finish.
@setEvalBranchQuota(100000);
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn main() !void {
// These are computed at compile time — zero runtime cost
const fib10 = comptime fibonacci(10);
const fib15 = comptime fibonacci(15);
const fib20 = comptime fibonacci(20);
std.debug.print("fib(10) = {}\n", .{fib10});
std.debug.print("fib(15) = {}\n", .{fib15});
std.debug.print("fib(20) = {}\n", .{fib20});
// Compile-time string processing
const message = comptime blk: {
var result: [5]u8 = "hello".*;
for (&result) |*c| {
c.* = std.ascii.toUpper(c.*);
}
break :blk result;
};
std.debug.print("Message: {s}\n", .{&message});
}
Because comptime code runs inside the compiler, Zig guards against accidental infinite loops with an evaluation budget — 1000 "backward branches" (loop iterations and recursive calls) by default. Naive recursive Fibonacci blows past that quickly, so fibonacci calls @setEvalBranchQuota to raise the limit. This is itself a piece of comptime tooling: it only affects compile-time evaluation and has no runtime cost.
Generic Functions with Comptime
Use comptime parameters to write generic functions — no special syntax needed.
const std = @import("std");
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
fn sum(comptime T: type, items: []const T) T {
var total: T = 0;
for (items) |item| {
total += item;
}
return total;
}
fn repeat(comptime T: type, value: T, comptime count: usize) [count]T {
return [_]T{value} ** count;
}
pub fn main() !void {
// Works with any numeric type
std.debug.print("max(i32): {}\n", .{max(i32, 10, 20)});
std.debug.print("max(f64): {d:.2}\n", .{max(f64, 3.14, 2.71)});
const ints = [_]i32{ 1, 2, 3, 4, 5 };
const floats = [_]f64{ 1.5, 2.5, 3.5 };
std.debug.print("sum(i32): {}\n", .{sum(i32, &ints)});
std.debug.print("sum(f64): {d:.1}\n", .{sum(f64, &floats)});
const fives = repeat(i32, 5, 4);
std.debug.print("repeat: ", .{});
for (fives) |v| std.debug.print("{} ", .{v});
std.debug.print("\n", .{});
}
Type Reflection with @typeInfo
Zig's built-in functions let you inspect types at compile time.
const std = @import("std");
const Point = struct {
x: f32,
y: f32,
z: f32 = 0,
};
fn printStructInfo(comptime T: type) void {
const info = @typeInfo(T);
switch (info) {
.@"struct" => |s| {
std.debug.print("Struct '{s}' has {} fields:\n", .{ @typeName(T), s.fields.len });
inline for (s.fields) |field| {
std.debug.print(" - {s}: {s}", .{ field.name, @typeName(field.type) });
if (field.default_value_ptr != null) {
std.debug.print(" (has default)", .{});
}
std.debug.print("\n", .{});
}
},
else => std.debug.print("Not a struct\n", .{}),
}
}
pub fn main() !void {
printStructInfo(Point);
// @typeInfo for basic types
std.debug.print("\ni32 is {}-bit integer\n", .{@typeInfo(i32).int.bits});
std.debug.print("f64 is {}-bit float\n", .{@typeInfo(f64).float.bits});
}
Compile-Time Arrays and Lookup Tables
Build data structures at compile time for instant runtime access.
const std = @import("std");
fn buildSquareTable(comptime size: usize) [size]u64 {
var table: [size]u64 = undefined;
for (0..size) |i| {
table[i] = i * i;
}
return table;
}
const square_table = buildSquareTable(16);
// Built once at compile time, then indexed with a runtime byte at zero cost.
const vowel_table = blk: {
var table: [256]bool = [_]bool{false} ** 256;
for ("aeiouAEIOU") |v| {
table[v] = true;
}
break :blk table;
};
fn isVowel(c: u8) bool {
return vowel_table[c];
}
pub fn main() !void {
std.debug.print("Square table:\n", .{});
for (0..16) |i| {
std.debug.print(" {}^2 = {}\n", .{ i, square_table[i] });
}
const word = "Hello World";
std.debug.print("\nVowels in \"{s}\": ", .{word});
for (word) |c| {
if (isVowel(c)) std.debug.print("{c} ", .{c});
}
std.debug.print("\n", .{});
}
Try It Yourself
const std = @import("std");
// Generic stack using comptime
fn Stack(comptime T: type, comptime max_size: usize) type {
return struct {
items: [max_size]T = undefined,
count: usize = 0,
const Self = @This();
fn push(self: *Self, value: T) !void {
if (self.count >= max_size) return error.StackOverflow;
self.items[self.count] = value;
self.count += 1;
}
fn pop(self: *Self) !T {
if (self.count == 0) return error.StackUnderflow;
self.count -= 1;
return self.items[self.count];
}
fn peek(self: *Self) !T {
if (self.count == 0) return error.StackUnderflow;
return self.items[self.count - 1];
}
fn isEmpty(self: *Self) bool {
return self.count == 0;
}
};
}
pub fn main() !void {
var int_stack = Stack(i32, 8){};
try int_stack.push(10);
try int_stack.push(20);
try int_stack.push(30);
std.debug.print("Top: {}\n", .{try int_stack.peek()});
std.debug.print("Pop: {}\n", .{try int_stack.pop()});
std.debug.print("Pop: {}\n", .{try int_stack.pop()});
std.debug.print("Empty: {}\n", .{int_stack.isEmpty()});
// Works with any type!
var float_stack = Stack(f64, 4){};
try float_stack.push(3.14);
try float_stack.push(2.71);
std.debug.print("\nFloat top: {d:.2}\n", .{try float_stack.peek()});
}
Key Takeaways
comptimeruns ordinary Zig code at compile time- Use
comptimeparameters for generic programming — no separate generics syntax @typeInfolets you inspect types for metaprogramming- Compile-time lookup tables have zero runtime cost
- Functions returning
typecreate generic data structures inline forunrolls loops over compile-time known data
Pro Tip: When you find yourself wanting macros or code generation, reach for
comptimefirst. Since it uses the same language as runtime code, you can test compile-time logic at runtime, then add thecomptimekeyword once it works.
Next Steps
Comptime showed you how Zig evaluates code at compile time. Next, you'll learn about optionals and unions -- two type-system features that let you model "maybe a value" and "one of several shapes" safely and expressively.
Next lesson
Optionals and Unions
Learn the Zig optional type and null safety — use ?T to represent nullable values, unwrap safely with orelse and if, and preview tagged unions for values with more than two shapes.
22 min