A compile-time loop is a loop that runs while Zig is compiling your program.
The loop does not run after the final executable starts. The compiler runs it earlier, uses the result, and then produces normal machine code.
This is useful when the loop works with information that is already known: types, fixed lists, array sizes, field names, enum tags, build options, or constant values.
Runtime Loops
A normal loop usually runs at runtime.
const std = @import("std");
pub fn main() void {
var i: usize = 0;
while (i < 3) : (i += 1) {
std.debug.print("{}\n", .{i});
}
}This prints:
0
1
2The loop counter i exists while the program is running. The program checks i < 3 each time.
Compile-Time Loops
Now place a loop inside a comptime block:
comptime {
var i: usize = 0;
while (i < 3) : (i += 1) {
@compileLog(i);
}
}This loop runs during compilation.
The final program does not contain this loop. The compiler has already executed it.
@compileLog prints messages from the compiler, not from the final program.
Looping to Build a Constant
A common use of compile-time loops is building fixed data.
const squares = comptime blk: {
var values: [5]u32 = undefined;
for (0..5) |i| {
values[i] = @intCast(i * i);
}
break :blk values;
};This creates this array during compilation:
.{ 0, 1, 4, 9, 16 }At runtime, the program already has the finished array. It does not need to compute the squares again.
inline for
inline for asks Zig to unroll the loop at compile time.
Example:
const std = @import("std");
pub fn main() void {
inline for (.{ "red", "green", "blue" }) |name| {
std.debug.print("{s}\n", .{name});
}
}This behaves like writing:
std.debug.print("{s}\n", .{"red"});
std.debug.print("{s}\n", .{"green"});
std.debug.print("{s}\n", .{"blue"});The loop is not a normal runtime loop. The compiler expands each iteration.
Why inline for Matters
A normal for loop uses one body repeatedly.
An inline for creates a separate body for each item.
That matters when each iteration has a different type.
const std = @import("std");
pub fn main() void {
inline for (.{ i32, bool, f64 }) |T| {
std.debug.print("{s}\n", .{@typeName(T)});
}
}Here, each item is a type.
Types are compile-time values. The compiler must know T for each iteration. inline for makes that possible.
The output is:
i32
bool
f64Normal Loops Cannot Iterate Over Types at Runtime
This does not make sense as a runtime loop:
for (.{ i32, bool, f64 }) |T| {
std.debug.print("{s}\n", .{@typeName(T)});
}The values i32, bool, and f64 are types. They belong to compile time. They are not ordinary runtime values.
Use inline for when the loop body depends on compile-time values such as types.
Compile-Time Loops with switch
Compile-time loops are useful with type-based branching.
const std = @import("std");
fn printDefaults() void {
inline for (.{ bool, u8, i32 }) |T| {
const value: T = switch (T) {
bool => false,
u8 => 0,
i32 => 0,
else => unreachable,
};
std.debug.print("{s}: {}\n", .{ @typeName(T), value });
}
}The compiler expands the loop once for each type.
Conceptually, it becomes:
{
const value: bool = false;
std.debug.print("{s}: {}\n", .{ "bool", value });
}
{
const value: u8 = 0;
std.debug.print("{s}: {}\n", .{ "u8", value });
}
{
const value: i32 = 0;
std.debug.print("{s}: {}\n", .{ "i32", value });
}This is not dynamic type inspection. The compiler already knows each type.
Repeating Code Without Macros
Many languages use macros for this kind of work.
Zig usually uses inline for and comptime.
Example:
fn isInteger(comptime T: type) bool {
inline for (.{ u8, u16, u32, u64, i8, i16, i32, i64 }) |Int| {
if (T == Int) {
return true;
}
}
return false;
}Usage:
const a = isInteger(u32); // true
const b = isInteger(bool); // falseThe loop runs during compilation because T is a compile-time type.
Compile-Time Loops Can Reject Code
You can use a compile-time loop to enforce rules.
fn requireInteger(comptime T: type) void {
inline for (.{ u8, u16, u32, u64, i8, i16, i32, i64 }) |Int| {
if (T == Int) {
return;
}
}
@compileError("expected an integer type");
}This works:
requireInteger(i32);This fails during compilation:
requireInteger(bool);The program does not reach runtime. Zig rejects the wrong type before producing the executable.
Building a Lookup Table
Suppose you want a table of byte values converted to hexadecimal characters.
fn makeHexTable() [16]u8 {
comptime var table: [16]u8 = undefined;
inline for (0..16) |i| {
table[i] = if (i < 10)
'0' + i
else
'a' + (i - 10);
}
return table;
}
const hex_chars = makeHexTable();The final hex_chars value is known at compile time.
It is equivalent to:
const hex_chars = [_]u8{
'0', '1', '2', '3',
'4', '5', '6', '7',
'8', '9', 'a', 'b',
'c', 'd', 'e', 'f',
};The loop helped you write the table clearly, but the runtime program receives the completed data.
inline while
Zig also supports inline while.
const std = @import("std");
pub fn main() void {
comptime var i: usize = 0;
inline while (i < 3) : (i += 1) {
std.debug.print("{}\n", .{i});
}
}The compiler expands each loop iteration.
This is less common than inline for, but it is useful when the loop shape is easier to express with a counter.
Runtime Cost
A compile-time loop can reduce runtime cost because the work happens before the program starts.
But this does not mean every loop should become compile-time.
Use compile-time loops for program structure and fixed data.
Use runtime loops for real input.
Good compile-time loop uses:
types
fields
enum tags
fixed tables
static configuration
known sizesGood runtime loop uses:
user input
file data
network data
database rows
command-line arguments
current timeA Common Mistake
Beginners sometimes try to use inline for for speed.
inline for (items) |item| {
process(item);
}This only works when items is compile-time known.
If items comes from a file or user input, it belongs to runtime. A normal for loop is correct.
Compile-time looping is about when information is known, not about making every loop faster.
Mental Model
A normal loop says:
Repeat this while the program runs.A compile-time loop says:
Repeat this while the compiler builds the program.An inline for says:
Create one copy of this code for each compile-time item.That is the core idea.
Compile-time loops let you write compact source code while still producing specialized runtime code.