@compileError em Zig — Referência e Exemplos

@compileError em Zig

O @compileError gera um erro de compilação com uma mensagem personalizada. É usado principalmente em código genérico e metaprogramação para rejeitar tipos ou configurações inválidos em tempo de compilação, antes que o programa seja executado. Esse mecanismo garante que problemas sejam detectados o mais cedo possível.

Sintaxe

@compileError(comptime msg: []const u8) noreturn

O que faz

O @compileError interrompe a compilação e exibe a mensagem fornecida como erro. O compilador aponta exatamente a linha onde o @compileError foi invocado, facilitando a identificação do problema. Esse builtin é especialmente poderoso em branches de switch ou if em comptime que representam caminhos de código inválidos.

Parâmetros

  • msg ([]const u8, comptime): A mensagem de erro a ser exibida pelo compilador. Deve ser uma string conhecida em tempo de compilação. Pode ser construída por concatenação de strings literais.

Valor de retorno

O tipo de retorno é noreturn — a compilação é interrompida e nenhum valor é produzido.

Exemplos práticos

Exemplo 1: Validação de tipos em funções genéricas

const std = @import("std");

fn somar(a: anytype, b: anytype) @TypeOf(a, b) {
    const T = @TypeOf(a, b);
    const info = @typeInfo(T);

    switch (info) {
        .int, .comptime_int => return a + b,
        .float, .comptime_float => return a + b,
        else => @compileError(
            "somar() requer tipos numéricos, recebeu: " ++ @typeName(T),
        ),
    }
}

test "somar numéricos" {
    try std.testing.expect(somar(@as(i32, 10), @as(i32, 20)) == 30);
    try std.testing.expect(somar(@as(f64, 1.5), @as(f64, 2.5)) == 4.0);

    // A linha abaixo geraria erro de compilação:
    // _ = somar("hello", "world");
    // Erro: somar() requer tipos numéricos, recebeu: *const [5:0]u8
}

Exemplo 2: Restrições de plataforma

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

fn funcaoApenasLinux() void {
    if (builtin.os.tag != .linux) {
        @compileError("Esta função só é suportada em Linux");
    }
    // Implementação específica de Linux...
}

fn funcaoApenas64Bits() void {
    if (@sizeOf(usize) != 8) {
        @compileError("Esta função requer uma arquitetura de 64 bits");
    }
    // Implementação que depende de ponteiros de 64 bits...
}

Exemplo 3: Validação de campos de struct em comptime

const std = @import("std");

fn validarModelo(comptime T: type) void {
    const info = @typeInfo(T);
    if (info != .@"struct") {
        @compileError("validarModelo requer uma struct, recebeu: " ++ @typeName(T));
    }

    const fields = info.@"struct".fields;

    // Verificar que a struct tem campo "id"
    comptime {
        var tem_id = false;
        for (fields) |f| {
            if (std.mem.eql(u8, f.name, "id")) {
                tem_id = true;
                break;
            }
        }
        if (!tem_id) {
            @compileError(@typeName(T) ++ " deve ter um campo 'id'");
        }
    }
}

const Usuario = struct {
    id: u64,
    nome: []const u8,
};

// Isso compila normalmente
comptime {
    validarModelo(Usuario);
}

// const Invalido = struct { nome: []const u8 };
// validarModelo(Invalido); // Erro: Invalido deve ter um campo 'id'

Casos de uso comuns

  1. Validação de tipos genéricos: Rejeitar tipos que não satisfazem os requisitos de uma função ou struct genérica.
  2. Restrições de plataforma: Impedir compilação em plataformas não suportadas.
  3. Funcionalidade não implementada: Marcar caminhos de código que ainda não foram implementados, diferente de @panic que ocorre em runtime.
  4. Validação de constantes: Verificar que constantes de configuração têm valores válidos.
  5. API segura: Guiar desenvolvedores com mensagens de erro claras quando usam uma API incorretamente.

Diferença entre @compileError e @panic

  • @compileError interrompe a compilação — o programa nunca chega a ser executado.
  • @panic interrompe a execução — o programa compila mas falha ao encontrar o panic em runtime.

Use @compileError quando o erro pode ser detectado estaticamente. Use @panic para erros que só podem ser detectados em tempo de execução.

Construindo mensagens de erro informativas

A mensagem de @compileError pode ser construída por concatenação de strings literais e @typeName:

fn requerInteiro(comptime T: type) void {
    const info = @typeInfo(T);
    if (info != .int and info != .comptime_int) {
        @compileError(
            "Esperado tipo inteiro, recebeu '" ++
            @typeName(T) ++
            "'. Use tipos como u8, i32, u64."
        );
    }
}

A mensagem aparece exatamente no ponto onde @compileError é chamado, com o stack de chamadas comptime:

src/lib.zig:5:9: error: Esperado tipo inteiro, recebeu 'f32'. Use tipos como u8, i32, u64.

@compileError em blocos comptime

@compileError é especialmente útil dentro de blocos comptime e inline for para validações complexas:

const Protocolo = struct {
    versao: u8,
    flags: u16,
    payload_len: u32,
};

comptime {
    // Verificar que o protocolo tem o tamanho esperado para o wire format
    if (@sizeOf(Protocolo) != 7) {
        @compileError(
            "Protocolo deve ter 7 bytes no wire format, mas tem " ++
            std.fmt.comptimePrint("{}", .{@sizeOf(Protocolo)}) ++
            " bytes. Verifique o layout."
        );
    }
}

Equivalente em C

Em C, _Static_assert (C11) é o equivalente mais próximo:

_Static_assert(sizeof(struct Protocolo) == 7, "Tamanho incorreto");

A diferença: _Static_assert tem mensagem estática. @compileError permite mensagens dinâmicas construídas com @typeName, @tagName e concatenação de strings comptime.

Padrão “else unreachable”

Um padrão comum é usar @compileError na branch else de um switch sobre tipos:

fn serializar(comptime T: type, valor: T) []const u8 {
    return switch (@typeInfo(T)) {
        .int => serializarInt(valor),
        .float => serializarFloat(valor),
        .bool => if (valor) "true" else "false",
        else => @compileError("Tipo não suportado para serialização: " ++ @typeName(T)),
    };
}

Isso garante que novos tipos precisem de implementação explícita — o compilador rejeita código que usa a função com tipos não suportados.

Erros comuns

Usar @compileError fora de contexto comptime com condição runtime: Se a condição que leva ao @compileError não for resolvível em comptime, o compilador emitirá um erro diferente — sobre variável runtime em contexto comptime.

String não-comptime na mensagem: A mensagem deve ser uma string literal ou concatenação de strings/valores comptime. Não pode ser uma string dinâmica de runtime.

Perguntas Frequentes

Posso usar std.fmt.comptimePrint para formatar a mensagem?

Sim! std.fmt.comptimePrint formata strings em comptime e pode ser concatenado na mensagem:

@compileError("Tamanho máximo é " ++ std.fmt.comptimePrint("{}", .{MAX}) ++ " bytes");

@compileError afeta o desempenho do binário?

Não. Se @compileError é atingido, a compilação falha e nenhum binário é gerado. Se não é atingido, não existe no código gerado — zero custo em runtime.

Builtins relacionados

  • @compileLog — Imprime valores em tempo de compilação para depuração
  • @typeName — Útil para construir mensagens de erro descritivas
  • @typeInfo — Inspeciona tipos para validação
  • @hasField — Verifica existência de campos
  • @hasDecl — Verifica existência de declarações

Tutoriais relacionados

Continue aprendendo Zig

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