What Is comptime
comptime means “compile time.”
In Zig, some code runs while your program is being compiled. That code does not run later when the final program starts. It runs earlier, inside the compiler, while Zig is checking and building your program.
This is one of Zig’s most important features.
Most languages have a sharp split:
You write source code.
The compiler turns it into a program.
The program runs later.
Zig keeps that basic model, but it also lets you execute normal Zig code during compilation. That means you can compute values, check rules, choose types, generate code patterns, and build generic functions before runtime.
Runtime vs Compile Time
Runtime means the time when the program is running.
Compile time means the time when the compiler is building the program.
Here is a runtime value:
const std = @import("std");
pub fn main() void {
const x = 10 + 20;
std.debug.print("{}\n", .{x});
}The expression 10 + 20 is simple, so the compiler may optimize it. But conceptually, this value belongs to the program.
Now compare this:
const x = comptime 10 + 20;This says: calculate this value during compilation.
The result is still 30, but Zig knows that x is a compile-time value.
A First Example
Suppose we want a function that returns the length of an array.
fn length(comptime n: usize) usize {
return n;
}The parameter n is marked comptime.
That means the caller must pass a value known during compilation:
const size = length(8);Here, 8 is known immediately. The compiler can run length(8) while compiling the program.
This is useful because some parts of Zig require compile-time values. Array lengths, types, and many code-shaping decisions must be known before the program runs.
Types Are Compile-Time Values
In Zig, types are values too.
That may feel strange at first. In many languages, types are only part of the compiler’s internal system. In Zig, you can pass a type to a function at compile time.
Example:
fn zero(comptime T: type) T {
return 0;
}This function takes a type called T, then returns a zero value of that type.
const a = zero(i32);
const b = zero(u64);For a, T is i32.
For b, T is u64.
The compiler creates the right version of the function based on the type you passed.
This is the foundation of Zig generics.
Generic Functions
A generic function is a function that works with more than one type.
Here is a simple example:
fn add(comptime T: type, a: T, b: T) T {
return a + b;
}This function accepts a type T, then accepts two values of that type.
const x = add(i32, 10, 20);
const y = add(f64, 1.5, 2.5);The first call works with i32.
The second call works with f64.
There is no separate template language. There is no macro syntax. Zig uses ordinary Zig code, with compile-time parameters.
comptime Makes Code Explicit
Zig does not want hidden magic.
If something must be known at compile time, Zig usually makes that visible.
This function says clearly that T is a compile-time argument:
fn makeDefault(comptime T: type) T {
return switch (T) {
bool => false,
i32 => 0,
u8 => 0,
else => @compileError("unsupported type"),
};
}Now the compiler can check the type and choose the correct branch while building the program.
const a = makeDefault(bool);
const b = makeDefault(i32);But this will fail:
const c = makeDefault([]u8);The compiler reaches this branch:
else => @compileError("unsupported type"),So the program does not compile.
This is powerful. You can reject bad code before it becomes a running program.
Compile-Time Errors
@compileError stops compilation with a custom error message.
Example:
fn onlySmall(comptime n: usize) void {
if (n > 10) {
@compileError("n must be 10 or smaller");
}
}This call is allowed:
onlySmall(5);This call fails during compilation:
onlySmall(20);The program never runs. The compiler rejects it first.
This is useful when you are writing libraries. You can make illegal use impossible, or at least make it fail with a clear message.
Compile-Time Branching
A normal if runs at runtime when its condition depends on runtime data.
But if the condition is known at compile time, Zig can choose the branch while compiling.
fn printTypeName(comptime T: type) void {
if (T == i32) {
@compileLog("i32");
} else if (T == bool) {
@compileLog("bool");
} else {
@compileLog("other");
}
}Here, T is known at compile time. So the compiler can decide which branch applies.
This is not like runtime polymorphism. There is no object, no virtual table, and no dynamic lookup. The compiler knows the answer early.
Compile-Time Loops
Zig can also run loops at compile time.
fn sum(comptime values: []const u32) u32 {
var total: u32 = 0;
for (values) |value| {
total += value;
}
return total;
}If values is known at compile time, the compiler can evaluate the loop while compiling.
const result = sum(&.{ 1, 2, 3, 4 });The result is 10, and it is known before the program runs.
inline for
Zig also has inline for, which expands loop iterations at compile time.
Example:
const std = @import("std");
pub fn main() void {
inline for (.{ i32, u8, bool }) |T| {
std.debug.print("{s}\n", .{@typeName(T)});
}
}The loop is unrolled by the compiler. Each iteration gets its own compile-time type value.
This is useful when writing generic code that needs to inspect several types.
Why Zig Uses comptime
Zig uses comptime instead of adding many separate language features.
In other languages, you may find:
macros
templates
generics
reflection
conditional compilation
build-time code generation
Zig handles many of these jobs with one idea: run normal Zig code at compile time.
That gives the language a smaller core. You do not need to learn a separate macro language. You use Zig itself.
What comptime Is Good For
comptime is useful when you need to work with information known before runtime.
Common uses include:
| Use | Example |
|---|---|
| Generic functions | A function that works with i32, u64, or f64 |
| Type selection | Choose a result type based on input types |
| Static checks | Reject unsupported types during compilation |
| Lookup tables | Build a table once at compile time |
| Code specialization | Generate fast code for a known case |
| Reflection | Inspect fields of a struct or tags of an enum |
| Configuration | Enable or disable code based on build options |
What comptime Is Not
comptime is not a way to make runtime data magically known early.
This does not work:
const std = @import("std");
pub fn main() void {
var x: usize = 10;
const y = comptime x + 1;
std.debug.print("{}\n", .{y});
}The variable x is a runtime variable. Its value belongs to the running program. Zig cannot use it as a compile-time value.
To use comptime, the value must be known while compiling.
A Simple Rule
Ask this question:
Can the compiler know this value before the program runs?
If yes, it may be usable at compile time.
If no, it is runtime data.
For example:
const a = 10;This can be known at compile time.
var b: usize = 10;This is runtime storage, because var means the value can change.
const T = i32;This is a compile-time type value.
const name = userInput();This depends on the running program, so it cannot be compile-time data.
The Mental Model
comptime lets the compiler execute part of your program before the final program exists.
That sounds advanced, but the basic idea is simple:
use runtime for work that depends on real input
use compile time for work that is already known
use compile-time checks to catch mistakes early
use compile-time types to write generic code
Zig’s comptime is not an extra layer added on top of the language. It is part of the language’s normal way of thinking.
When you understand comptime, Zig starts to make much more sense.