Skip to content

Compile-Time Variables

In the previous section, you learned that Zig can execute code during compilation.

In the previous section, you learned that Zig can execute code during compilation.

Now we will look at compile-time variables.

A compile-time variable is a variable that exists while the compiler is building the program. Its value is known completely before runtime begins.

This is different from normal runtime variables.

Runtime Variables

Here is a normal runtime variable:

pub fn main() void {
    var x: i32 = 10;
    x += 5;
}

The variable x exists while the program runs.

The CPU creates storage for it at runtime. The value may change during execution.

Compile-Time Variables

Now look at this:

comptime {
    const x = 10;
    const y = x + 5;
}

This block runs during compilation.

The variables x and y exist only inside the compiler while it is building the program.

They do not exist in the final executable.

The comptime Block

A comptime block forces code to execute during compilation.

Example:

const std = @import("std");

comptime {
    std.debug.print("hello\n", .{});
}

When the compiler processes this file, the block executes immediately.

The output appears during compilation, not when the final program runs.

This is an important distinction.

Runtime code runs after you start the executable.

Compile-time code runs while Zig is still generating the executable.

Compile-Time Constants

Many constants are naturally compile-time values.

const a = 100;
const b = a * 2;

Because these values are fully known, Zig can compute them during compilation.

You often do not need to write comptime explicitly. Zig automatically recognizes many compile-time expressions.

Compile-Time Expressions

An expression is compile-time when all required information is already known.

Example:

const size = 4 * 8;

The compiler already knows both numbers, so it computes the result immediately.

But this is different:

var runtime_value: usize = 4;
const size = runtime_value * 8;

Here, runtime_value is a runtime variable.

Its value is not guaranteed to be known during compilation.

Therefore size is not a compile-time expression.

Array Lengths Must Be Compile-Time Known

One of the most common places where compile-time values are required is array lengths.

const numbers: [4]i32 = .{ 1, 2, 3, 4 };

The compiler must know the size of the array before the program runs.

This works:

const count = 4;

const numbers: [count]i32 = .{
    1,
    2,
    3,
    4,
};

Because count is compile-time known.

But this fails:

var count: usize = 4;

const numbers: [count]i32 = undefined;

The compiler cannot use a runtime variable as an array length.

Compile-Time Function Parameters

You already saw this pattern:

fn square(comptime n: i32) i32 {
    return n * n;
}

The parameter n must be known during compilation.

const result = square(5);

This works.

But this fails:

var x: i32 = 5;
const result = square(x);

Why?

Because x is a runtime variable.

Even though its value currently happens to be 5, Zig treats it as runtime data because it may change later.

Compile-Time Loops

You can use loops during compilation too.

Example:

comptime {
    var sum: u32 = 0;

    for (0..5) |i| {
        sum += i;
    }
}

The loop executes inside the compiler.

No runtime code is generated for this loop.

Using inline for

inline for is closely connected to compile-time execution.

Example:

const std = @import("std");

pub fn main() void {
    inline for (.{ 1, 2, 3 }) |value| {
        std.debug.print("{}\n", .{value});
    }
}

The compiler unrolls the loop.

Conceptually, Zig transforms this:

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

This is useful for metaprogramming and specialization.

Compile-Time Mutable Variables

You can use mutable variables at compile time.

comptime {
    var total: i32 = 0;

    total += 10;
    total += 20;
}

The variable changes while the compiler executes the block.

But again, this variable exists only during compilation.

It disappears after compilation finishes.

Building Data at Compile Time

Compile-time execution is useful for generating data structures ahead of time.

Example:

const table = comptime blk: {
    var values: [4]u32 = undefined;

    for (0..4) |i| {
        values[i] = @intCast(i * i);
    }

    break :blk values;
};

This creates the array during compilation.

The resulting array becomes part of the final program.

The loop itself does not run at runtime.

Understanding comptime Blocks

A comptime block is an instruction to the compiler:

Execute this code now.

Example:

comptime {
    @compileLog("building program");
}

@compileLog prints information during compilation.

This is useful for debugging compile-time logic.

Compile-Time Types

Types themselves are compile-time values.

comptime {
    const T = i32;
}

The variable T stores a type.

This is completely normal in Zig.

You can compare types too:

fn printInfo(comptime T: type) void {
    if (T == i32) {
        @compileLog("signed integer");
    }
}

The compiler evaluates the condition while compiling.

Compile-Time Decisions

Compile-time variables allow Zig to specialize code.

Example:

fn maxValue(comptime T: type) T {
    return switch (T) {
        u8 => 255,
        u16 => 65535,
        else => @compileError("unsupported type"),
    };
}

The compiler selects the correct branch based on the compile-time type.

const a = maxValue(u8);
const b = maxValue(u16);

Each call becomes specialized code.

Runtime Variables Cannot Become Compile-Time Values

This is one of the most important rules in Zig.

Runtime data cannot travel backward into compilation.

Example:

fn getNumber() i32 {
    return 42;
}

pub fn main() void {
    const x = getNumber();
}

Even though getNumber() always returns 42, Zig still treats this as runtime behavior unless the function itself is evaluated at compile time.

The compiler must be able to prove the value is known during compilation.

Compile-Time Memory

Compile-time variables exist only inside the compiler process.

They are not stored in the final executable unless their values are embedded into generated data.

For example:

const values = comptime blk: {
    var array: [3]u8 = undefined;

    array[0] = 10;
    array[1] = 20;
    array[2] = 30;

    break :blk array;
};

The compiler builds the array during compilation, then stores the final result inside the executable.

The temporary compile-time variables disappear afterward.

Compile-Time Safety

Compile-time execution is still checked by Zig’s safety rules.

Example:

comptime {
    const numbers = [_]u8{ 1, 2, 3 };
    const x = numbers[10];

    _ = x;
}

This fails during compilation because the array index is invalid.

The compiler catches the mistake immediately.

Why Compile-Time Variables Matter

Compile-time variables are one of the foundations of Zig’s design.

They allow Zig to:

FeatureBenefit
Generic functionsReuse code across types
ReflectionInspect types during compilation
Code specializationGenerate optimized code
Static validationCatch mistakes early
Build-time generationCreate tables and structures ahead of time
Zero-cost abstractionsRemove runtime overhead

Without compile-time execution, Zig would need separate systems for templates, macros, and code generation.

Instead, Zig uses ordinary Zig code during compilation.

Mental Model

Think of Zig as having two execution worlds:

WorldWhen It Runs
Compile timeWhile the compiler builds the program
RuntimeAfter the executable starts

Compile-time variables belong to the first world.

Runtime variables belong to the second world.

The compiler can move information forward:

compile time → runtime

But runtime information cannot move backward:

runtime → compile time

That rule explains many parts of Zig’s behavior.