Zig Comptime Cheat Sheet
Covers Zig's comptime keyword, compile-time function execution, generic types via comptime parameters, and comptime reflection with @TypeOf/@typeInfo.
`comptime` Values & Blocks
`comptime` forces an expression to be evaluated at compile time, with a normal Zig block/expression syntax.
const std = @import("std");fn fibonacci(n: u32) u32 { if (n < 2) return n; return fibonacci(n - 1) + fibonacci(n - 2);}pub fn main() void { // computed entirely at compile time, baked into the binary const result = comptime fibonacci(10); std.debug.print("fib(10) = {}\n", .{result}); comptime { // arbitrary compile-time logic block var sum: u32 = 0; var i: u32 = 0; while (i < 5) : (i += 1) sum += i; }}
Generics via `comptime` Parameters
Zig has no separate generics syntax — types are just comptime-known values passed as parameters.
fn max(comptime T: type, a: T, b: T) T { return if (a > b) a else b;}const m = max(i32, 3, 7); // T = i32const f = max(f64, 1.5, 2.25); // T = f64// Generic container: a function returning a typefn Stack(comptime T: type) type { return struct { items: std.ArrayList(T), pub fn init(allocator: std.mem.Allocator) @This() { return .{ .items = std.ArrayList(T).init(allocator) }; } pub fn push(self: *@This(), value: T) !void { try self.items.append(value); } };}const IntStack = Stack(i32);
Compile-Time Reflection
`@TypeOf`, `@typeInfo`, and `@field` let you inspect and generate code based on types.
const std = @import("std");fn printFields(value: anytype) void { const T = @TypeOf(value); const info = @typeInfo(T); inline for (info.Struct.fields) |field| { std.debug.print("{s}: {any}\n", .{ field.name, @field(value, field.name) }); }}const Point = struct { x: i32, y: i32 };pub fn main() void { printFields(Point{ .x = 1, .y = 2 }); // x: 1 // y: 2}
Comptime-Known Arrays & `inline for`
Build lookup tables and unroll loops entirely at compile time.
fn buildSquares(comptime n: usize) [n]u32 { var table: [n]u32 = undefined; for (&table, 0..) |*slot, i| { slot.* = @as(u32, @intCast(i * i)); } return table;}const squares = comptime buildSquares(10); // computed at compile timepub fn main() void { // inline for unrolls the loop at compile time (n must be comptime-known) inline for (squares) |sq| { @import("std").debug.print("{} ", .{sq}); }}
Comptime-Related Builtins & Keywords
Reference for the core compile-time toolbox.
- comptime- forces an expression, parameter, or var to be known/evaluated at compile time
- @TypeOf(x)- returns the type of an expression, itself a comptime value
- @typeInfo(T)- returns a struct describing T's fields/kind for reflection
- @field(obj, name)- accesses a field by comptime-known string name
- inline for / inline while- unrolls loops at compile time; bounds must be comptime-known
- anytype- parameter type inferred per call site, resolved at compile time
- @compileError(msg)- aborts compilation with a custom message, used for comptime validation
Default Field Values via Comptime Expressions
Struct field defaults can be arbitrary comptime expressions, not just literals, letting you derive one field's default from another.
const std = @import("std");const BufferSize = 64;const Config = struct { capacity: usize = BufferSize, // default computed at compile time from another comptime constant half: usize = BufferSize / 2, name: []const u8 = "default-" ++ "config",};// Comptime-only field: type itself can be a default-valued fieldconst Wrapper = struct { comptime marker: u32 = 0xdead_beef, payload: []const u8,};pub fn main() void { const c = Config{}; std.debug.print("{s} half={}\n", .{ c.name, c.half });}
Compile-Time Interfaces via `anytype` + Duck Typing
Zig has no `interface` keyword; generic code accepts `anytype` and the compiler enforces the required method set structurally at each call site.
fn draw(shape: anytype) void { // no explicit interface — this only compiles if @TypeOf(shape) has .area() const a = shape.area(); @import("std").debug.print("area = {d}\n", .{a});}const Circle = struct { r: f64, pub fn area(self: Circle) f64 { return std.math.pi * self.r * self.r; }};const Square = struct { side: f64, pub fn area(self: Square) f64 { return self.side * self.side; }};const std = @import("std");pub fn main() void { draw(Circle{ .r = 2.0 }); draw(Square{ .side = 3.0 }); // draw(42) would fail to compile: i32 has no .area()}
Comptime String Concatenation & Formatting
`++` and `**` operate on comptime-known arrays/slices; `std.fmt.comptimePrint` builds formatted strings entirely at compile time.
const std = @import("std");// ++ concatenates comptime-known arrays/stringsconst greeting = "Hello, " ++ "Zig!";// ** repeats a comptime-known arrayconst separator = "-" ** 20;// comptimePrint builds a formatted string at compile time (no allocator needed)const version_str = std.fmt.comptimePrint("v{d}.{d}.{d}", .{ 0, 14, 1 });const Table = struct { fn headerFor(comptime name: []const u8) []const u8 { return std.fmt.comptimePrint("=== {s} ===", .{name}); }};pub fn main() void { std.debug.print("{s}\n{s}\n{s}\n", .{ greeting, separator, version_str });}
Comptime-Generated Dispatch Over a Tagged Union
`inline for` over `@typeInfo(T).Union.fields` generates a specialized branch per variant, avoiding a runtime jump table when the union is small and known.
const std = @import("std");const Shape = union(enum) { circle: f64, square: f64, rect: struct { w: f64, h: f64 },};fn area(shape: Shape) f64 { // switch on a tagged union is itself resolved per-variant at compile time return switch (shape) { .circle => |r| std.math.pi * r * r, .square => |s| s * s, .rect => |d| d.w * d.h, };}fn describeAllVariants() void { // walks the union's field metadata at compile time to emit docs/logging inline for (@typeInfo(Shape).Union.fields) |field| { std.debug.print("variant: {s}\n", .{field.name}); }}
Comptime Gotchas & Constraints
Rules that trip up developers coming from languages with runtime generics or templates.
- No recursive comptime types- a type function cannot reference itself directly (e.g. `fn T() type { return T(); }` never terminates and is rejected)
- comptime var mutation is per-instantiation- a `comptime var` inside a generic function is reset for each distinct set of comptime arguments, not shared globally
- Loop bounds must be comptime-known for `inline for`- runtime-length slices cannot drive `inline for`; use a regular `for` instead
- Comptime branch quota- deeply recursive comptime evaluation can hit the compiler's default `@setEvalBranchQuota`; raise it explicitly for heavy metaprogramming
- @This() inside generic functions- refers to the struct being defined, letting methods reference the not-yet-named generic instantiation
- No implicit comptime→runtime type erasure- you cannot pass a `type` value into a function that only takes runtime parameters; it must stay comptime
Use comptime parameters plus @compileError to validate generic type constraints at the top of a function (e.g. "T must be an integer type") — you get a clear, custom compiler error instead of a confusing failure deep inside the function body.