Skip to content

Inline Loops

Zig has normal loops that run when the program runs.

Zig has normal loops that run when the program runs.

for (items) |item| {
    process(item);
}

Zig also has inline loops.

An inline loop is expanded by the compiler.

That means the compiler does not treat it as an ordinary runtime loop. Instead, it repeats the loop body during compilation and generates code from the result.

The two forms are:

inline for (items) |item| {
    // repeated at compile time
}

and:

inline while (condition) : (update) {
    // repeated at compile time
}

For beginners, the most important one is inline for.

A Normal for Loop

Start with a normal loop:

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3 };

    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

This prints:

1
2
3

This loop runs at runtime.

When the program runs, it walks through the array and prints each item.

An inline for Loop

Now compare this:

const std = @import("std");

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3 };

    inline for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

It prints the same output:

1
2
3

But the meaning is different.

The compiler expands the loop. Conceptually, it becomes something like:

std.debug.print("{}\n", .{1});
std.debug.print("{}\n", .{2});
std.debug.print("{}\n", .{3});

This is not always the exact generated code, but it is the right mental model.

An inline for says: repeat this code at compile time for each item.

Why Inline Loops Exist

Inline loops are useful when the loop controls code generation.

That usually means one of these cases:

You are looping over types.

You are looping over fields of a struct.

You are generating repeated code at compile time.

You need each iteration to be known separately by the compiler.

A normal runtime loop is for data.

An inline loop is often for code structure.

Looping Over Types

A normal runtime loop cannot loop over types.

Types exist at compile time.

const std = @import("std");

pub fn main() void {
    const types = .{ u8, u16, u32 };

    inline for (types) |T| {
        std.debug.print("{} has size {}\n", .{ T, @sizeOf(T) });
    }
}

This prints something like:

u8 has size 1
u16 has size 2
u32 has size 4

The tuple:

.{ u8, u16, u32 }

contains types.

Each iteration gives a type named T.

Then this expression:

@sizeOf(T)

asks the compiler for the size of that type.

A normal for loop cannot do this, because types are not runtime values.

Inline Loops and Tuples

Inline loops are common with tuples.

A tuple can hold values of different types:

const values = .{ 123, true, "zig" };

The first item is an integer.

The second item is a boolean.

The third item is a string.

A normal for loop expects items with a consistent runtime shape. An inline loop can handle each tuple field separately:

const std = @import("std");

pub fn main() void {
    const values = .{ 123, true, "zig" };

    inline for (values) |value| {
        std.debug.print("{}\n", .{value});
    }
}

Each iteration is compiled separately, so Zig can type-check each value with its own type.

Inline Loops and Struct Fields

One powerful use of inline for is reflection.

Reflection means inspecting program structure, such as the fields of a type.

const std = @import("std");

const User = struct {
    id: u32,
    name: []const u8,
    active: bool,
};

pub fn main() void {
    inline for (@typeInfo(User).@"struct".fields) |field| {
        std.debug.print("{s}\n", .{field.name});
    }
}

This prints:

id
name
active

This line asks Zig for information about User:

@typeInfo(User)

The struct information contains its fields.

The inline loop walks through those fields at compile time.

This is the kind of code that makes Zig’s compile-time system useful. You can write code that understands types without using a separate macro language.

Runtime Loop vs Inline Loop

A runtime loop repeats work while the program runs.

for (items) |item| {
    use(item);
}

An inline loop repeats code while the program is being compiled.

inline for (items) |item| {
    use(item);
}

The difference matters.

A runtime loop is good for a list of values loaded from a file, read from the network, or built while the program runs.

An inline loop requires compile-time-known input. The compiler must know what it is expanding.

This works:

const values = .{ 1, 2, 3 };

inline for (values) |x| {
    _ = x;
}

This does not fit the purpose of inline for:

var values = [_]u8{ 1, 2, 3 };

inline for (values) |x| {
    _ = x;
}

If the loop input is ordinary runtime data, use a normal for.

Inline Loops Can Reduce Abstraction Cost

Suppose you want to run the same test for several integer types.

const std = @import("std");

fn maxValue(comptime T: type) T {
    return std.math.maxInt(T);
}

test "max values" {
    const types = .{ u8, u16, u32 };

    inline for (types) |T| {
        try std.testing.expect(maxValue(T) > 0);
    }
}

The compiler expands the loop into separate checks.

Conceptually:

try std.testing.expect(maxValue(u8) > 0);
try std.testing.expect(maxValue(u16) > 0);
try std.testing.expect(maxValue(u32) > 0);

Each call is specialized for its type.

This gives you generic-looking code without hiding what the compiler must generate.

Inline Loops Are Not for Everything

Do not use inline for just because it looks faster.

This:

for (items) |item| {
    process(item);
}

is usually what you want for ordinary data.

This:

inline for (items) |item| {
    process(item);
}

asks the compiler to duplicate the loop body for each item. That can increase code size.

Inline loops are best when the compiler needs each iteration to be separate.

Good uses:

Looping over types.

Looping over tuple fields.

Looping over struct fields.

Generating code from compile-time-known data.

Avoid it for ordinary arrays that should be processed at runtime.

inline while

Zig also supports inline while.

const std = @import("std");

pub fn main() void {
    comptime var i = 0;

    inline while (i < 3) : (i += 1) {
        std.debug.print("i = {}\n", .{i});
    }
}

This prints:

i = 0
i = 1
i = 2

The variable i is a compile-time variable:

comptime var i = 0;

The loop is expanded during compilation.

For beginners, inline while is less common than inline for. You will mostly see it in advanced compile-time code.

A Practical Example: Generate a Parser Table

Imagine you have a fixed set of keywords:

const keywords = .{
    "if",
    "else",
    "while",
    "return",
};

You can use inline for to generate checks:

const std = @import("std");

fn isKeyword(text: []const u8) bool {
    const keywords = .{
        "if",
        "else",
        "while",
        "return",
    };

    inline for (keywords) |keyword| {
        if (std.mem.eql(u8, text, keyword)) {
            return true;
        }
    }

    return false;
}

pub fn main() void {
    std.debug.print("{}\n", .{isKeyword("while")});
}

This prints:

true

The keyword list is known at compile time.

The compiler can expand the checks.

For a small fixed list, this is simple and clear. For a large list, you may want a different structure such as a hash table. Inline code generation should still be used with judgment.

The Main Idea

An inline loop is a compile-time loop.

A normal loop repeats work at runtime.

An inline loop repeats code during compilation.

Use normal loops for ordinary data:

for (items) |item| {
    process(item);
}

Use inline loops when the loop is part of compile-time code generation:

inline for (types) |T| {
    useType(T);
}

The beginner rule is simple: do not reach for inline first. Write a normal loop unless the compiler needs each iteration to be known separately.