Comptime em Zig: O Poder da Execução em Tempo de Compilação

Se existe uma feature que define o que torna Zig especial, é o comptime. Enquanto linguagens como C dependem de um preprocessador baseado em substituição de texto, e C++ usa templates com sintaxe críptica, Zig permite que você execute código real da linguagem em tempo de compilação — com a mesma sintaxe, as mesmas regras e a mesma depurabilidade do código que roda em tempo de execução.

Neste tutorial, vamos explorar comptime em profundidade: desde os conceitos básicos até técnicas avançadas de metaprogramação que farão você repensar o que é possível em uma linguagem de sistemas. Se você ainda está começando com Zig, recomendamos ler a Introdução ao Zig antes de continuar.

O que é Comptime?

comptime é uma palavra-chave do Zig que instrui o compilador a avaliar expressões durante a compilação em vez de em tempo de execução. Mas vai muito além de simples constantes — em Zig, você pode executar funções inteiras, iterar sobre arrays, manipular tipos e até gerar structs completas, tudo durante a compilação.

A Ideia Central

A filosofia do comptime é: não crie uma linguagem separada para metaprogramação. Em vez disso, use a mesma linguagem que você já conhece.

const std = @import("std");

fn fibonacci(n: u16) u16 {
    if (n == 0 or n == 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

pub fn main() void {
    // Calculado em TEMPO DE COMPILAÇÃO
    // O binário já contém o valor 55 — zero custo em runtime
    const fib10 = comptime fibonacci(10);

    std.debug.print("fibonacci(10) = {}\n", .{fib10});
}

Observe: fibonacci é uma função normal. Não existe nenhuma anotação especial que a torna “compatível com comptime”. O compilador simplesmente executa a função durante a compilação quando você usa a palavra-chave comptime.

O que Comptime Pode Fazer

CapacidadeExemplo
Calcular valores constantescomptime fibonacci(10)
Definir tamanhos de arraysvar arr: [comptime tamanho()]u8 = undefined;
Criar tipos genéricosfn ArrayList(comptime T: type) type { ... }
Reflexão sobre tipos@typeInfo(T) para inspecionar structs, enums, etc.
Gerar código condicionalmenteif (comptime builtin.os.tag == .linux) { ... }
Validar entradas em compilação@compileError("mensagem")
Desdobrar loopsinline for sobre tuplas conhecidas em comptime

O que Comptime NÃO Pode Fazer

Para manter a linguagem previsível, comptime tem restrições intencionais:

  • Não pode fazer I/O: sem acesso a arquivos, rede ou stdin/stdout durante compilação.
  • Não pode alocar memória em heap: sem malloc ou allocators em comptime.
  • Não depende da arquitetura do host: o código comptime não sabe em qual máquina está compilando (isso garante builds reproduzíveis).
  • Não pode chamar funções externas C (como printf) em comptime.

Comptime vs Macros C/C++

Programadores vindos de C frequentemente perguntam: “Comptime substitui macros?”. A resposta é sim — e é imensamente superior. Para uma comparação completa entre Zig e C, veja nosso tutorial dedicado.

Comparação Direta

Macro C — MAX genérico:

// Problemas:
// 1. Sem verificação de tipos
// 2. Efeitos colaterais (avaliação dupla de argumentos)
// 3. Impossível depurar com gdb/lldb
// 4. Mensagens de erro incompreensíveis

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int resultado = MAX(x++, 3);
// BUG: x++ avaliado DUAS vezes! resultado imprevisível

Zig — Função comptime genérica:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

pub fn main() void {
    // Seguro, tipado, depurável
    var x: i32 = 5;
    const resultado = max(i32, x, 3);
    // x avaliado UMA vez. Comportamento previsível.

    // Erro de compilação se T não suportar '>'
    // const errado = max([]const u8, "a", "b");
    // ^^^ erro: operador '>' não definido para []const u8
}

Compilação Condicional

C:

#ifdef DEBUG
    printf("debug: x = %d\n", x);
#endif

#ifdef _WIN32
    #include <windows.h>
#elif __linux__
    #include <unistd.h>
#endif

Zig:

const builtin = @import("builtin");
const std = @import("std");

pub fn main() void {
    // Compilação condicional — mas é código REAL, não texto
    if (builtin.mode == .Debug) {
        std.debug.print("debug: x = {}\n", .{x});
    }
    // O compilador elimina esse bloco em ReleaseFast

    // Detecção de plataforma
    const os = builtin.os.tag;
    if (os == .windows) {
        // Código Windows
    } else if (os == .linux) {
        // Código Linux
    }
}

Tabela Comparativa

AspectoMacros C/C++Templates C++Comptime Zig
LinguagemSubstituição de textoMeta-linguagem separadaMesma linguagem
Verificação de tipos❌ Nenhuma✅ Parcial✅ Completa
Depuração❌ Impossível❌ Muito difícil✅ Normal
Mensagens de erro❌ Crípticas❌ Páginas de erros✅ Claras e localizadas
Efeitos colaterais❌ Possíveis✅ Sem✅ Sem
Recursão❌ Limitada✅ Sim✅ Sim
Geração de tipos❌ Não✅ Sim✅ Sim

Variáveis e Blocos Comptime

Variáveis comptime

Uma variável comptime existe apenas durante a compilação e nunca aparece no binário final:

pub fn main() void {
    // Variável comptime — calculada durante compilação
    comptime var x: i32 = 0;
    x += 1;
    x += 1;
    x += 1;
    // x agora vale 3, mas esse cálculo aconteceu no compilador

    // Atribuir a uma constante runtime
    const resultado: i32 = x; // No binário, isso é simplesmente "3"

    std.debug.print("resultado = {}\n", .{resultado});
}

Blocos comptime

Você pode marcar um bloco inteiro como comptime para executar lógica complexa durante a compilação:

const std = @import("std");

pub fn main() void {
    // Bloco comptime com resultado
    const mensagem = comptime blk: {
        var buf: [64]u8 = undefined;
        const texto = "ZIG";
        var i: usize = 0;
        for (texto) |c| {
            buf[i] = c;
            i += 1;
            buf[i] = ' ';
            i += 1;
        }
        break :blk buf[0..i];
    };

    std.debug.print("Mensagem: {s}\n", .{mensagem});
    // Saída: "Mensagem: Z I G "
}

comptime_int e comptime_float

Literais numéricos em Zig são de tipos especiais que só existem em comptime:

const std = @import("std");

pub fn main() void {
    // Literais são comptime_int — precisão arbitrária!
    const grande = 100_000_000_000_000_000_000;
    // ^^^ isso não caberia em u64, mas comptime_int suporta

    // Ao atribuir a um tipo concreto, o compilador verifica o range
    const x: u8 = 42;      // OK: 42 cabe em u8
    // const y: u8 = 256;   // ERRO: 256 não cabe em u8

    // comptime_float é f128 internamente
    const pi = 3.14159265358979323846;
    const pi_f32: f32 = pi; // Conversão com possível perda de precisão
    const pi_f64: f64 = pi; // Mais precisão preservada

    std.debug.print("pi f32 = {d:.15}\n", .{pi_f32});
    std.debug.print("pi f64 = {d:.15}\n", .{pi_f64});
}

Funções com Parâmetros Comptime

Aqui as coisas ficam realmente interessantes. Quando um parâmetro de função é marcado como comptime, o valor deve ser conhecido em tempo de compilação. Isso é a base dos generics em Zig.

Generics Básicos

const std = @import("std");

// Função genérica que funciona com qualquer tipo numérico
fn somar(comptime T: type, a: T, b: T) T {
    return a + b;
}

pub fn main() void {
    // O compilador gera versões especializadas para cada tipo
    const r1 = somar(i32, 10, 20);       // Versão para i32
    const r2 = somar(f64, 3.14, 2.71);   // Versão para f64
    const r3 = somar(u8, 100, 50);       // Versão para u8

    std.debug.print("i32: {}, f64: {d:.2}, u8: {}\n", .{ r1, r2, r3 });
}

Funções que Retornam Tipos

Em Zig, tipos são valores de primeira classe em comptime. Uma função pode receber e retornar tipos, incluindo structs, enums e unions:

const std = @import("std");

// Função que cria um tipo de array com tamanho customizado
fn Array(comptime T: type, comptime tamanho: comptime_int) type {
    return [tamanho]T;
}

// Função que cria um tipo de par (tupla tipada)
fn Par(comptime A: type, comptime B: type) type {
    return struct {
        primeiro: A,
        segundo: B,

        const Self = @This();

        pub fn criar(a: A, b: B) Self {
            return .{ .primeiro = a, .segundo = b };
        }

        pub fn trocar(self: Self) Par(B, A) {
            return Par(B, A).criar(self.segundo, self.primeiro);
        }
    };
}

pub fn main() void {
    // Tipo Array de 5 inteiros
    const MeuArray = Array(i32, 5);
    var arr: MeuArray = .{ 1, 2, 3, 4, 5 };
    arr[0] = 10;

    // Tipo Par de string e inteiro
    const par = Par([]const u8, i32).criar("idade", 30);
    std.debug.print("{s} = {}\n", .{ par.primeiro, par.segundo });

    // Trocar os elementos do par
    const invertido = par.trocar();
    std.debug.print("{} = {s}\n", .{ invertido.primeiro, invertido.segundo });
}

anytype — Inferência em Comptime

Quando um parâmetro é declarado como anytype, o compilador infere o tipo automaticamente. Isso funciona como “templates” implícitos:

const std = @import("std");

// anytype permite aceitar qualquer tipo
fn dobrar(valor: anytype) @TypeOf(valor) {
    return valor + valor;
}

fn imprimir(valor: anytype) void {
    const T = @TypeOf(valor);
    switch (@typeInfo(T)) {
        .int, .comptime_int => std.debug.print("Inteiro: {}\n", .{valor}),
        .float, .comptime_float => std.debug.print("Float: {d:.4}\n", .{valor}),
        .pointer => |ptr_info| {
            if (ptr_info.size == .Slice and ptr_info.child == u8) {
                std.debug.print("String: {s}\n", .{valor});
            } else {
                std.debug.print("Ponteiro: {*}\n", .{valor});
            }
        },
        else => std.debug.print("Tipo: {}\n", .{@typeName(T)}),
    }
}

pub fn main() void {
    std.debug.print("{}\n", .{dobrar(@as(i32, 21))}); // 42
    std.debug.print("{d}\n", .{dobrar(@as(f64, 1.5))}); // 3.0

    imprimir(@as(i32, 42));
    imprimir(@as(f64, 3.14));
    imprimir("Olá Zig!");
}

Tipos como Valores de Primeira Classe

Em Zig, type é um tipo real que pode ser armazenado em variáveis, passado para funções e retornado — tudo em comptime. Isso é radicalmente diferente de qualquer outra linguagem de sistemas.

Manipulando Tipos

const std = @import("std");

pub fn main() void {
    // Tipos podem ser armazenados em constantes
    const MeuTipo = i32;
    var x: MeuTipo = 42;
    x += 1;

    // Tipos podem ser escolhidos condicionalmente
    const Numero = if (@sizeOf(usize) >= 8) i64 else i32;
    const valor: Numero = 1000;

    std.debug.print("Tamanho de Numero: {} bytes\n", .{@sizeOf(Numero)});
    std.debug.print("Valor: {}\n", .{valor});

    // Branching sobre tipos em comptime
    const tipo_escolhido = comptime blk: {
        const arquitetura = @import("builtin").cpu.arch;
        break :blk switch (arquitetura) {
            .x86_64 => f64,
            .aarch64 => f64,
            else => f32,
        };
    };

    std.debug.print("Tipo de float para esta arquitetura: {}\n", .{@typeName(tipo_escolhido)});
}

Criando Tipos Dinamicamente com @Type

A builtin @Type permite construir tipos programaticamente a partir de uma descrição:

const std = @import("std");

// Criar um tipo inteiro com N+1 bits
fn InteiroMaior(comptime T: type) type {
    const info = @typeInfo(T).int;
    return @Type(.{
        .int = .{
            .bits = info.bits + 1,
            .signedness = info.signedness,
        },
    });
}

// Criar um tipo inteiro sem sinal a partir de um com sinal
fn SemSinal(comptime T: type) type {
    const info = @typeInfo(T).int;
    return @Type(.{
        .int = .{
            .bits = info.bits,
            .signedness = .unsigned,
        },
    });
}

test "manipulação de tipos" {
    // u8 → u9
    try std.testing.expect(InteiroMaior(u8) == u9);

    // i32 → i33
    try std.testing.expect(InteiroMaior(i32) == i33);

    // i64 → u64
    try std.testing.expect(SemSinal(i64) == u64);

    // i16 → u16
    try std.testing.expect(SemSinal(i16) == u16);
}

Geração de Código em Comptime

Uma das aplicações mais poderosas de comptime é a geração de dados e código durante a compilação — sem nenhum custo em tempo de execução.

Lookup Tables

const std = @import("std");

// Gerar tabela de quadrados em comptime
fn gerarTabelaQuadrados(comptime tamanho: usize) [tamanho]u64 {
    var tabela: [tamanho]u64 = undefined;
    for (0..tamanho) |i| {
        tabela[i] = i * i;
    }
    return tabela;
}

// Gerar tabela de senos pré-calculados (para games/DSP)
fn gerarTabelaSenos(comptime pontos: usize) [pontos]f64 {
    var tabela: [pontos]f64 = undefined;
    const passo = 2.0 * std.math.pi / @as(f64, @floatFromInt(pontos));
    for (0..pontos) |i| {
        tabela[i] = @sin(passo * @as(f64, @floatFromInt(i)));
    }
    return tabela;
}

// CRC32 lookup table — usada em checksum de dados
fn gerarCRC32Table() [256]u32 {
    var tabela: [256]u32 = undefined;
    for (0..256) |i| {
        var crc: u32 = @intCast(i);
        for (0..8) |_| {
            if (crc & 1 == 1) {
                crc = (crc >> 1) ^ 0xEDB88320;
            } else {
                crc = crc >> 1;
            }
        }
        tabela[i] = crc;
    }
    return tabela;
}

// Todas estas tabelas são calculadas em TEMPO DE COMPILAÇÃO
// e incorporadas ao binário como dados estáticos
const QUADRADOS = comptime gerarTabelaQuadrados(100);
const SENOS = comptime gerarTabelaSenos(360);
const CRC32_TABLE = comptime gerarCRC32Table();

pub fn main() void {
    std.debug.print("7² = {}\n", .{QUADRADOS[7]}); // 49
    std.debug.print("sin(90°) ≈ {d:.6}\n", .{SENOS[90]}); // ~1.0
    std.debug.print("CRC32[0] = 0x{X:0>8}\n", .{CRC32_TABLE[0]}); // 0x00000000
    std.debug.print("CRC32[1] = 0x{X:0>8}\n", .{CRC32_TABLE[1]}); // 0x77073096
}

💡 Por que isso importa? Em C, para gerar essas tabelas em tempo de compilação, você precisaria de scripts Python/Perl gerando código C, ou macros absurdamente complexas. Em Zig, é apenas uma função normal com a palavra-chave comptime.

Implementações Condicionais

Use comptime para selecionar a melhor implementação com base nas características da plataforma (veja também cross-compilation em Zig):

const std = @import("std");
const builtin = @import("builtin");

fn somaOtimizada(slice: []const i32) i64 {
    // Se a CPU suporta SIMD, use instruções vetoriais
    if (comptime std.Target.x86.featureSetHas(builtin.cpu.features, .avx2)) {
        return somaAVX2(slice);
    } else if (comptime std.Target.x86.featureSetHas(builtin.cpu.features, .sse2)) {
        return somaSSE2(slice);
    } else {
        return somaEscalar(slice);
    }
}

fn somaEscalar(slice: []const i32) i64 {
    var total: i64 = 0;
    for (slice) |valor| {
        total += valor;
    }
    return total;
}

fn somaSSE2(slice: []const i32) i64 {
    // Implementação SSE2 aqui
    return somaEscalar(slice); // fallback simplificado
}

fn somaAVX2(slice: []const i32) i64 {
    // Implementação AVX2 aqui
    return somaEscalar(slice); // fallback simplificado
}

O compilador remove completamente os branches que não se aplicam à plataforma alvo. O binário final contém apenas a implementação escolhida.

@typeInfo e Reflexão em Comptime

@typeInfo é a ferramenta de reflexão do Zig. Ela retorna uma tagged union que descreve completamente qualquer tipo. Combinada com comptime, permite criar código que se adapta automaticamente a qualquer tipo de dado.

Inspecionando Tipos

const std = @import("std");

fn descreverTipo(comptime T: type) void {
    const info = @typeInfo(T);
    switch (info) {
        .int => |i| {
            const sinal = if (i.signedness == .signed) "com sinal" else "sem sinal";
            @compileLog("Inteiro " ++ sinal, i.bits, "bits");
        },
        .float => |f| {
            @compileLog("Float de", f.bits, "bits");
        },
        .@"struct" => |s| {
            @compileLog("Struct com", s.fields.len, "campos");
            for (s.fields) |campo| {
                @compileLog("  campo:", campo.name);
            }
        },
        .pointer => |p| {
            @compileLog("Ponteiro para", @typeName(p.child));
        },
        else => @compileLog("Outro tipo:", @typeName(T)),
    }
}

const Pessoa = struct {
    nome: []const u8,
    idade: u32,
    ativo: bool,
};

// Chamado em comptime — imprime informações no log de compilação
comptime {
    descreverTipo(i32);       // "Inteiro com sinal, 32 bits"
    descreverTipo(f64);       // "Float de 64 bits"
    descreverTipo(Pessoa);    // "Struct com 3 campos"
    descreverTipo(*const u8); // "Ponteiro para u8"
}

Iterando sobre Campos de uma Struct

Este é um padrão extremamente útil — percorrer todos os campos de uma struct em comptime:

const std = @import("std");

const Configuracao = struct {
    host: []const u8 = "localhost",
    porta: u16 = 8080,
    max_conexoes: u32 = 100,
    debug: bool = false,
    timeout_ms: u64 = 5000,
};

fn imprimirCampos(comptime T: type, valor: T) void {
    const info = @typeInfo(T).@"struct";
    std.debug.print("=== {} ===\n", .{@typeName(T)});
    inline for (info.fields) |campo| {
        const v = @field(valor, campo.name);
        std.debug.print("  {s}: {any}\n", .{ campo.name, v });
    }
}

pub fn main() void {
    const config = Configuracao{
        .host = "meuservidor.com",
        .porta = 3000,
    };
    imprimirCampos(Configuracao, config);
}

Saída:

=== Configuracao ===
  host: meuservidor.com
  porta: 3000
  max_conexoes: 100
  debug: false
  timeout_ms: 5000

Serialização Automática para JSON

Combinando reflexão com comptime, podemos criar um serializador genérico:

const std = @import("std");

fn paraJSON(comptime T: type, valor: T, writer: anytype) !void {
    const info = @typeInfo(T);
    switch (info) {
        .@"struct" => |s| {
            try writer.writeAll("{");
            var primeiro = true;
            inline for (s.fields) |campo| {
                if (!primeiro) try writer.writeAll(",");
                primeiro = false;
                try writer.print("\"{s}\":", .{campo.name});
                try paraJSON(campo.type, @field(valor, campo.name), writer);
            }
            try writer.writeAll("}");
        },
        .int, .comptime_int => try writer.print("{}", .{valor}),
        .float, .comptime_float => try writer.print("{d}", .{valor}),
        .bool => try writer.print("{}", .{valor}),
        .pointer => |ptr| {
            if (ptr.size == .Slice and ptr.child == u8) {
                try writer.print("\"{s}\"", .{valor});
            }
        },
        .optional => {
            if (valor) |v| {
                try paraJSON(@typeInfo(T).optional.child, v, writer);
            } else {
                try writer.writeAll("null");
            }
        },
        else => try writer.writeAll("null"),
    }
}

const Produto = struct {
    nome: []const u8,
    preco: f64,
    estoque: u32,
    disponivel: bool,
};

pub fn main() !void {
    const produto = Produto{
        .nome = "Teclado Mecânico",
        .preco = 299.90,
        .estoque = 42,
        .disponivel = true,
    };

    const stdout = std.io.getStdOut().writer();
    try paraJSON(Produto, produto, stdout);
    try stdout.writeAll("\n");
}

Saída:

{"nome":"Teclado Mecânico","preco":299.9,"estoque":42,"disponivel":true}

O mais impressionante: o compilador conhece todos os campos em tempo de compilação, então o código gerado é tão eficiente quanto escrever a serialização na mão para cada tipo.

Exemplos Práticos

Exemplo 1: ArrayList Genérico

Vamos implementar uma versão simplificada de ArrayList — a estrutura de dados mais usada em Zig. Este exemplo também demonstra o uso de allocators:

const std = @import("std");
const Allocator = std.mem.Allocator;

fn ArrayList(comptime T: type) type {
    return struct {
        items: []T,
        capacidade: usize,
        tamanho: usize,
        allocator: Allocator,

        const Self = @This();

        pub fn init(allocator: Allocator) Self {
            return .{
                .items = &[_]T{},
                .capacidade = 0,
                .tamanho = 0,
                .allocator = allocator,
            };
        }

        pub fn deinit(self: *Self) void {
            if (self.capacidade > 0) {
                self.allocator.free(self.items.ptr[0..self.capacidade]);
            }
        }

        pub fn append(self: *Self, item: T) !void {
            if (self.tamanho >= self.capacidade) {
                try self.crescer();
            }
            self.items.ptr[self.tamanho] = item;
            self.tamanho += 1;
            self.items.len = self.tamanho;
        }

        pub fn get(self: Self, indice: usize) T {
            if (indice >= self.tamanho) {
                @panic("índice fora dos limites");
            }
            return self.items[indice];
        }

        pub fn slice(self: Self) []const T {
            return self.items.ptr[0..self.tamanho];
        }

        fn crescer(self: *Self) !void {
            const nova_capacidade = if (self.capacidade == 0)
                8
            else
                self.capacidade * 2;

            const novo_buf = try self.allocator.alloc(T, nova_capacidade);

            if (self.tamanho > 0) {
                @memcpy(novo_buf[0..self.tamanho], self.items.ptr[0..self.tamanho]);
            }

            if (self.capacidade > 0) {
                self.allocator.free(self.items.ptr[0..self.capacidade]);
            }

            self.items.ptr = novo_buf.ptr;
            self.items.len = self.tamanho;
            self.capacidade = nova_capacidade;
        }

        // Informações de tipo disponíveis em comptime
        pub const Item = T;
        pub const tamanho_item = @sizeOf(T);
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // ArrayList de inteiros
    var numeros = ArrayList(i32).init(allocator);
    defer numeros.deinit();

    try numeros.append(10);
    try numeros.append(20);
    try numeros.append(30);

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

    // ArrayList de strings
    var nomes = ArrayList([]const u8).init(allocator);
    defer nomes.deinit();

    try nomes.append("Ana");
    try nomes.append("Bruno");
    try nomes.append("Carlos");

    for (nomes.slice()) |nome| {
        std.debug.print("{s} ", .{nome});
    }
    std.debug.print("\n", .{});

    // Meta-informação disponível em comptime
    std.debug.print("Tamanho de cada item (i32): {} bytes\n", .{ArrayList(i32).tamanho_item});
    std.debug.print("Tamanho de cada item (f64): {} bytes\n", .{ArrayList(f64).tamanho_item});
}

Exemplo 2: Formatador Customizado com comptime

Implementando format para suas structs, você integra com todo o sistema de formatação do Zig:

const std = @import("std");

const Vetor3D = struct {
    x: f64,
    y: f64,
    z: f64,

    const Self = @This();

    // O formato do comptime fmt permite customizar a saída
    pub fn format(
        self: Self,
        comptime fmt: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = options;

        if (comptime std.mem.eql(u8, fmt, "coords")) {
            // Formato de coordenadas: (x, y, z)
            try writer.print("({d:.2}, {d:.2}, {d:.2})", .{ self.x, self.y, self.z });
        } else if (comptime std.mem.eql(u8, fmt, "mag")) {
            // Formato de magnitude
            const mag = @sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
            try writer.print("|v| = {d:.4}", .{mag});
        } else if (fmt.len == 0) {
            // Formato padrão
            try writer.print("Vec3({d:.2}, {d:.2}, {d:.2})", .{ self.x, self.y, self.z });
        } else {
            // Formato não reconhecido — erro de COMPILAÇÃO
            std.fmt.invalidFmtError(fmt, self);
        }
    }

    pub fn soma(self: Self, outro: Self) Self {
        return .{
            .x = self.x + outro.x,
            .y = self.y + outro.y,
            .z = self.z + outro.z,
        };
    }
};

pub fn main() void {
    const v = Vetor3D{ .x = 1.0, .y = 2.0, .z = 3.0 };

    std.debug.print("Padrão:     {}\n", .{v});
    std.debug.print("Coordenadas: {coords}\n", .{v});
    std.debug.print("Magnitude:  {mag}\n", .{v});

    // O compilador valida o formato em TEMPO DE COMPILAÇÃO
    // std.debug.print("{invalido}\n", .{v}); // Erro de compilação!
}

Saída:

Padrão:     Vec3(1.00, 2.00, 3.00)
Coordenadas: (1.00, 2.00, 3.00)
Magnitude:  |v| = 3.7417

Exemplo 3: Gerador de HashMap Perfeito em Comptime

Quando as chaves são conhecidas em tempo de compilação, podemos gerar um mapa otimizado:

const std = @import("std");

fn MapaEstatico(
    comptime chaves: []const []const u8,
    comptime V: type,
) type {
    return struct {
        valores: [chaves.len]V,

        const Self = @This();

        // Busca O(n) mas com N pequeno e inline, é mais rápido
        // que hash map para poucos elementos
        pub fn get(self: Self, chave: []const u8) ?V {
            inline for (chaves, 0..) |k, i| {
                if (std.mem.eql(u8, k, chave)) {
                    return self.valores[i];
                }
            }
            return null;
        }

        pub fn init(valores: [chaves.len]V) Self {
            return .{ .valores = valores };
        }

        // Validação em comptime: chaves duplicadas
        comptime {
            for (chaves, 0..) |a, i| {
                for (chaves[i + 1 ..]) |b| {
                    if (std.mem.eql(u8, a, b)) {
                        @compileError("Chaves duplicadas no mapa: " ++ a);
                    }
                }
            }
        }
    };
}

pub fn main() void {
    // Mapa de HTTP status codes
    const StatusMap = MapaEstatico(
        &.{ "200", "201", "301", "400", "404", "500" },
        []const u8,
    );

    const status = StatusMap.init(.{
        "OK",
        "Created",
        "Moved Permanently",
        "Bad Request",
        "Not Found",
        "Internal Server Error",
    });

    // Busca é otimizada pelo compilador
    if (status.get("404")) |desc| {
        std.debug.print("404 = {s}\n", .{desc});
    }
    if (status.get("200")) |desc| {
        std.debug.print("200 = {s}\n", .{desc});
    }
    if (status.get("999")) |desc| {
        std.debug.print("999 = {s}\n", .{desc});
    } else {
        std.debug.print("999 = não encontrado\n", .{});
    }
}

inline for e Desdobramento de Loops

O inline for é essencial quando você precisa iterar sobre dados conhecidos em comptime enquanto o corpo do loop contém operações runtime:

const std = @import("std");

// Sistema de validação genérico
fn Validador(comptime T: type) type {
    return struct {
        const Regra = struct {
            nome: []const u8,
            validar: *const fn (T) bool,
        };

        pub fn validarTudo(
            valor: T,
            comptime regras: []const Regra,
        ) !void {
            inline for (regras) |regra| {
                if (!regra.validar(valor)) {
                    std.debug.print("❌ Falhou: {s}\n", .{regra.nome});
                    return error.ValidacaoFalhou;
                }
                std.debug.print("✅ Passou: {s}\n", .{regra.nome});
            }
        }
    };
}

fn ehPositivo(n: i32) bool {
    return n > 0;
}

fn ehPar(n: i32) bool {
    return @mod(n, 2) == 0;
}

fn menorQue100(n: i32) bool {
    return n < 100;
}

pub fn main() !void {
    const V = Validador(i32);
    const regras = [_]V.Regra{
        .{ .nome = "positivo", .validar = &ehPositivo },
        .{ .nome = "par", .validar = &ehPar },
        .{ .nome = "menor que 100", .validar = &menorQue100 },
    };

    std.debug.print("Validando 42:\n", .{});
    try V.validarTudo(42, &regras);

    std.debug.print("\nValidando -5:\n", .{});
    V.validarTudo(-5, &regras) catch {
        std.debug.print("Validação falhou (esperado)\n", .{});
    };
}

Boas Práticas e Armadilhas Comuns

✅ Boas Práticas

1. Use comptime para eliminar overhead de abstração:

// BOM: genérico sem custo em runtime
fn comparar(comptime T: type, a: T, b: T) std.math.Order {
    return std.math.order(a, b);
}

2. Valide entradas em comptime com @compileError (veja também testes em Zig para validação em runtime):

fn criarBuffer(comptime tamanho: usize) [tamanho]u8 {
    if (tamanho == 0) {
        @compileError("Buffer não pode ter tamanho zero");
    }
    if (tamanho > 1024 * 1024) {
        @compileError("Buffer na stack não deve exceder 1MB");
    }
    return undefined;
}

3. Use inline for apenas quando necessário:

// inline for é necessário quando o corpo depende do tipo do campo
fn somarCamposNumericos(comptime T: type, valor: T) f64 {
    var soma: f64 = 0;
    inline for (@typeInfo(T).@"struct".fields) |campo| {
        if (@typeInfo(campo.type) == .int or @typeInfo(campo.type) == .float) {
            soma += @as(f64, @floatFromInt(@field(valor, campo.name)));
        }
    }
    return soma;
}

4. Nomeie funções que retornam tipos com PascalCase:

// Convenção do Zig: funções que retornam type usam PascalCase
fn HashMap(comptime K: type, comptime V: type) type { ... }
fn ArrayList(comptime T: type) type { ... }

// Funções normais usam camelCase
fn calcularTotal(items: []const Item) u64 { ... }

❌ Armadilhas Comuns

1. Não confunda comptime var com const:

pub fn main() void {
    // ATENÇÃO: comptime var é avaliada pelo compilador
    comptime var x: i32 = 0;
    x += 1; // Isso roda no COMPILADOR, não em runtime

    // Para variáveis runtime, use var normalmente
    var y: i32 = 0;
    y += 1; // Isso roda em RUNTIME
}

2. Cuidado com o tamanho do binário:

// CUIDADO: inline for com muitos elementos gera muito código
fn processar(dados: [1000]u32) void {
    // Isso gera 1000 cópias do corpo do loop!
    inline for (dados) |d| {
        fazAlgo(d);
    }

    // MELHOR: use for normal quando inline não é necessário
    for (dados) |d| {
        fazAlgo(d);
    }
}

3. Não force comptime desnecessariamente:

// RUIM: forçar comptime quando não há benefício
fn somar(a: i32, b: i32) i32 {
    return comptime a + b; // ERRO: a e b não são conhecidos em comptime
}

// BOM: use comptime onde o valor PODE ser conhecido em compilação
fn criarArray(comptime tamanho: usize) [tamanho]u8 {
    return [_]u8{0} ** tamanho;
}

4. Entenda os limites de memória do comptime:

// CUIDADO: o compilador tem limites de memória para avaliação comptime
fn gerarGrande() [1_000_000]u64 {
    // Pode ser lento ou falhar durante compilação
    // Tabelas muito grandes devem ser geradas externamente
    // e embedadas com @embedFile
    var tabela: [1_000_000]u64 = undefined;
    for (0..1_000_000) |i| {
        tabela[i] = i * i;
    }
    return tabela;
}

Dica Final: @compileLog para Depuração

@compileLog é o printf do comptime. Use para inspecionar valores durante a compilação:

fn meuGenerico(comptime T: type) type {
    // Imprime no terminal DURANTE a compilação
    @compileLog("Criando tipo para:", @typeName(T));
    @compileLog("Tamanho:", @sizeOf(T), "bytes");
    @compileLog("Alinhamento:", @alignOf(T), "bytes");

    return struct {
        dados: T,
    };
}

⚠️ Nota: @compileLog causa um erro de compilação proposital — remova antes de fazer o build final. É apenas uma ferramenta de depuração.

Resumo: O que Você Aprendeu

ConceitoO que fazQuando usar
comptime keywordForça avaliação em tempo de compilaçãoCálculos de constantes, tamanhos de array
Parâmetros comptimePermite genericsFunções que operam sobre tipos
type como valorTipos são valores de primeira classeCriar estruturas de dados genéricas
@typeInfoReflexão sobre tiposSerialização, validação, geração de código
@TypeCriar tipos programaticamenteManipulação avançada de tipos
inline forDesdobra loops em comptimeIterar sobre campos de structs
@compileErrorGerar erros em compilaçãoValidar parâmetros comptime
@compileLogDebug print em compilaçãoDepuração de código comptime

Próximos Passos

Agora que você domina comptime, está pronto para ir mais fundo:

  1. 📖 Está começando com Zig? — Comece pelo Guia de Instalação para configurar seu ambiente.
  2. 🔄 Vem do C? — Veja como comptime substitui macros no nosso Guia de Migração C para Zig.
  3. 📚 Documentação oficial — A seção sobre comptime no Language Reference é a referência canônica.
  4. 🧪 Pratique — Reescreva uma macro C complexa que você usa como uma função comptime em Zig.
  5. 🏗️ Zig Build System — Veja como comptime é usado no sistema de build do Zig.
  6. 🔍 Estude a std — O código-fonte da biblioteca padrão do Zig é um masterclass em uso de comptime.

Este tutorial é parte da série avançada do ZigLang Brasil. Se comptime mudou sua forma de pensar sobre metaprogramação, compartilhe com outros desenvolvedores!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.