# Generating Code at Compile Time

### Generating Code at Compile Time

Generating code at compile time means using Zig code to create specialized program behavior before the final executable is built.

This does not mean Zig writes a new `.zig` file for you.

It means the compiler executes compile-time logic and uses the result to build the final program.

In Zig, compile-time code generation usually comes from:

| Feature | What It Does |
|---|---|
| `comptime` parameters | Pass types, sizes, flags, or static choices into a function |
| `inline for` | Expand repeated code during compilation |
| `switch` on types | Select code based on a compile-time type |
| `@typeInfo` | Inspect the structure of a type |
| `@Type` | Build a type from type information |
| `@compileError` | Reject invalid generated code early |

The important point is simple: Zig uses normal Zig code for this.

#### Code Generation Without Text Generation

Some languages generate code by producing text.

For example, a tool might write a file containing source code, then compile that file.

Zig usually avoids that pattern.

Instead of generating text, Zig generates program structure directly.

Example:

```zig
fn makePair(comptime T: type) type {
    return struct {
        first: T,
        second: T,
    };
}
```

This function returns a type.

Use it like this:

```zig
const IntPair = makePair(i32);

const value = IntPair{
    .first = 10,
    .second = 20,
};
```

The compiler builds a struct type whose fields both use `i32`.

If you call:

```zig
const BoolPair = makePair(bool);
```

The compiler builds a different struct type whose fields both use `bool`.

There is no string manipulation. There is no separate template file. Zig creates the type directly.

#### Returning a Type from a Function

This is one of the first major ideas to understand.

In Zig, a function can return a type if the function runs at compile time.

```zig
fn Buffer(comptime size: usize) type {
    return struct {
        data: [size]u8,
    };
}
```

This function takes a compile-time size and returns a new struct type.

```zig
const SmallBuffer = Buffer(16);
const LargeBuffer = Buffer(4096);
```

Now `SmallBuffer` and `LargeBuffer` are different types.

`SmallBuffer` contains:

```zig
data: [16]u8
```

`LargeBuffer` contains:

```zig
data: [4096]u8
```

The size is baked into the type.

#### Why This Is Useful

This pattern is useful when the structure of the program depends on compile-time information.

For example, a fixed-size buffer should know its size at compile time:

```zig
fn FixedBuffer(comptime size: usize) type {
    return struct {
        data: [size]u8 = undefined,
        len: usize = 0,

        pub fn append(self: *@This(), byte: u8) !void {
            if (self.len >= size) {
                return error.NoSpaceLeft;
            }

            self.data[self.len] = byte;
            self.len += 1;
        }
    };
}
```

Usage:

```zig
const Buffer32 = FixedBuffer(32);

pub fn main() !void {
    var buffer = Buffer32{};

    try buffer.append('A');
    try buffer.append('B');
}
```

The compiler knows the buffer size. The type itself carries the size.

This gives you reusable code without runtime overhead for storing or checking a dynamic capacity field beyond what you choose to include.

#### `@This()`

Inside an anonymous struct, `@This()` refers to the current type.

In this method:

```zig
pub fn append(self: *@This(), byte: u8) !void {
    if (self.len >= size) {
        return error.NoSpaceLeft;
    }

    self.data[self.len] = byte;
    self.len += 1;
}
```

`@This()` means “the struct type being created.”

For `FixedBuffer(32)`, `@This()` means that specific buffer type.

For `FixedBuffer(1024)`, it means a different buffer type.

This is why the same source code can produce different specialized types.

#### Generating Repeated Code with `inline for`

`inline for` can generate repeated code paths.

Suppose you want to check whether a type is one of several allowed integer types:

```zig
fn isAllowedInteger(comptime T: type) bool {
    inline for (.{ u8, u16, u32, u64 }) |Allowed| {
        if (T == Allowed) {
            return true;
        }
    }

    return false;
}
```

The compiler expands the loop for each type.

Conceptually, it becomes:

```zig
if (T == u8) return true;
if (T == u16) return true;
if (T == u32) return true;
if (T == u64) return true;
return false;
```

The loop is source-level convenience. The compiler turns it into direct checks.

#### Generating Specialized Functions

You can generate a type that contains functions.

```zig
fn Counter(comptime T: type) type {
    return struct {
        value: T = 0,

        pub fn increment(self: *@This()) void {
            self.value += 1;
        }

        pub fn get(self: @This()) T {
            return self.value;
        }
    };
}
```

Usage:

```zig
const U8Counter = Counter(u8);
const U64Counter = Counter(u64);

pub fn main() void {
    var small = U8Counter{};
    var large = U64Counter{};

    small.increment();
    large.increment();
}
```

The compiler creates counter types specialized for `u8` and `u64`.

Each generated type has its own `value` field and methods.

#### Generating Code Based on a Flag

Compile-time flags can choose code shape.

```zig
fn Logger(comptime enabled: bool) type {
    return struct {
        pub fn log(message: []const u8) void {
            if (enabled) {
                @import("std").debug.print("{s}\n", .{message});
            }
        }
    };
}
```

Usage:

```zig
const DebugLogger = Logger(true);
const SilentLogger = Logger(false);
```

For `Logger(false)`, the compiler sees that `enabled` is false. The log body has no runtime work.

This is a common pattern for debug features, optional checks, and static configuration.

#### Generating APIs from Fields

A more advanced use is inspecting fields of a struct.

For that, Zig uses `@typeInfo`.

Here is the idea:

```zig
fn printFieldNames(comptime T: type) void {
    const info = @typeInfo(T);

    inline for (info.@"struct".fields) |field| {
        @compileLog(field.name);
    }
}
```

Given a struct type, Zig can inspect its fields during compilation.

Example:

```zig
const User = struct {
    id: u64,
    name: []const u8,
};

comptime {
    printFieldNames(User);
}
```

The compiler can see:

```text
id
name
```

This is the beginning of reflection-based code generation.

You can use this idea to build serializers, command-line parsers, database mappers, validation systems, and formatters.

#### Rejecting Invalid Generated Code

When generating code, you should reject bad inputs early.

```zig
fn RequireStruct(comptime T: type) void {
    const info = @typeInfo(T);

    if (info != .@"struct") {
        @compileError("expected a struct type");
    }
}
```

Usage:

```zig
const User = struct {
    id: u64,
};

comptime {
    RequireStruct(User);
}
```

This is accepted.

But this fails:

```zig
comptime {
    RequireStruct(u32);
}
```

The compiler stops with your error message.

This gives library authors a way to produce clear failures instead of confusing type errors.

#### Code Generation and Runtime Cost

Compile-time generation can remove runtime cost.

For example, this function specializes on a type:

```zig
fn zero(comptime T: type) T {
    return switch (T) {
        bool => false,
        u8 => 0,
        u32 => 0,
        else => @compileError("unsupported type"),
    };
}
```

When you call:

```zig
const x = zero(u8);
```

The compiler knows the branch. The final code does not need to check the type at runtime.

There is no runtime type object, no reflection lookup, and no dynamic dispatch.

The decision happened during compilation.

#### Code Generation Can Increase Binary Size

Specialization has a cost.

If you generate many versions of the same logic for many types, the final program may become larger.

For example:

```zig
const A = Counter(u8);
const B = Counter(u16);
const C = Counter(u32);
const D = Counter(u64);
```

Each type may produce specialized code.

That can be good for performance, but it can also increase the binary size.

So compile-time generation should be used deliberately.

#### Do Not Overuse It

Code generation is powerful, but it can make code harder to read if every small decision happens at compile time.

Prefer ordinary runtime code when the decision depends on runtime data.

Use compile-time code generation when the structure of the program depends on information known before the program starts.

Good uses:

| Good Use | Why |
|---|---|
| Generic containers | Type is known at compile time |
| Fixed-size buffers | Size is part of the type |
| Static feature flags | Compiler can remove unused code |
| Reflection over structs | Fields are known at compile time |
| Formatters and serializers | Type structure is known early |
| Lookup tables | Data can be precomputed |

Poor uses:

| Poor Use | Why |
|---|---|
| User input | Known only at runtime |
| File contents | Usually known only at runtime |
| Network data | Arrives after the program starts |
| Large unnecessary specialization | Can bloat the binary |
| Clever tricks | Makes code harder to maintain |

#### Mental Model

Compile-time code generation in Zig means:

write ordinary Zig code

run part of it during compilation

use the result to shape types, functions, checks, or constants

emit a normal executable

The compiler is not just translating your code. It is also allowed to execute the parts of your code that are marked or required to run at compile time.

That is why Zig’s generic programming feels different from many languages.

You are not using a separate macro system.

You are programming the compiler with Zig itself.

