Skip to content

Comptime

Se há um ponto em que Zig deixa de parecer “um C mais seguro” e vira outra coisa, é o comptime. A ideia é simples de enunciar, mas suas consequências são enormes: código Zig comum pode rodar durante a compilação, e nesse regime os tipos são valores de primeira classe, do tipo type. Não existe uma segunda linguagem de macros, não existe um sistema de templates com sua própria gramática. É a mesma linguagem, executada mais cedo, e essa unificação é o que faz genéricos, reflexão e geração de código serem todos o mesmo mecanismo visto de ângulos diferentes.

A afirmação de que um tipo é um valor precisa ser levada ao pé da letra. Você pode ter uma variável cujo valor é u32, passá-la a uma função, retorná-la, guardá-la num campo. A única regra é que, sendo um valor que só existe na compilação, ela vive em contexto comptime. Um parâmetro marcado comptime é resolvido antes de o programa rodar, e um parâmetro de tipo type é necessariamente assim. É isso que torna um genérico apenas uma função comum:

generico-funcao.zig
fn maior(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// Cada instanciação é uma versão especializada, gerada em tempo de compilação.
const x = maior(u32, 3, 9);
const y = maior(f64, 2.5, 1.0);

Não há apagamento de tipo e não há custo em tempo de execução: maior(u32, ...) e maior(f64, ...) compilam para duas funções distintas, cada uma tão eficiente quanto se você a tivesse escrito à mão. É monomorfização, como em Rust ou nos templates de C++, só que expressa na sintaxe da linguagem comum, sem um dialeto à parte.

Genéricos são funções que devolvem tipos

Section titled “Genéricos são funções que devolvem tipos”

O passo que destrava as estruturas de dados genéricas é notar que, se um tipo é um valor, uma função pode devolvê-lo. Uma coleção genérica em Zig é uma função que recebe um type e retorna um struct:

generico-struct.zig
fn Pilha(comptime T: type) type {
return struct {
itens: std.ArrayListUnmanaged(T) = .{},
fn push(self: *@This(), a: std.mem.Allocator, v: T) !void {
try self.itens.append(a, v);
}
};
}
const PilhaDeU32 = Pilha(u32); // um tipo concreto, fabricado na compilação

Pilha(u32) chama uma função em tempo de compilação que devolve um struct novo, e @This() é como o struct anônimo se refere a si mesmo. É assim que std.ArrayList, std.HashMap e praticamente todo contêiner da biblioteca padrão são definidos: não como sintaxe especial de genéricos, mas como funções comptime que produzem tipos. Entender isso é entender de onde a std tira seu poder.

Como o código de compilação é código de verdade, ele pode inspecionar tipos. O @typeInfo(T) devolve uma tagged union que descreve a estrutura de T: seus campos, se é struct, enum ou union, os tipos de cada campo. Combine isso com o inline for, um laço que o compilador desenrola em tempo de compilação, e você gera código a partir da forma de um tipo. É essa combinação que sustenta serializadores, formatadores e o std.json, todos capazes de percorrer um tipo desconhecido e emitir o código certo para ele.

reflexao.zig
fn imprimirCampos(valor: anytype) void {
const T = @TypeOf(valor);
inline for (@typeInfo(T).@"struct".fields) |campo| {
std.debug.print("{s} = {any}\n", .{ campo.name, @field(valor, campo.name) });
}
}

O inline for aqui não é um laço em tempo de execução: para um struct de três campos, o compilador gera três chamadas de print distintas, cada uma com o nome e o tipo do campo já resolvidos. A função funciona para qualquer struct, e ainda assim não paga nada por essa generalidade em tempo de execução. É reflexão sem o custo habitual da reflexão, porque tudo acontece antes de o programa existir.

Por que não é como os generics de TypeScript

Section titled “Por que não é como os generics de TypeScript”

Vale contrastar com o outro sistema de tipos poderoso que esta base cobre, os generics de TypeScript, porque a diferença esclarece os dois. Eles resolvem problemas parecidos partindo de premissas opostas:

Zig comptimeGenerics de TypeScript
É a mesma linguagem?sim: if, for e funções comunsnão: uma linguagem de tipos à parte
O resultado é…código gerado e especializadotipos apagados antes do runtime
Roda quando?na compilação, produzindo código de máquinasó na checagem de tipos
Ganha em…um só modelo mental e código especializadoanálise expressiva sobre código que não controla o runtime

Os generics de TypeScript formam uma linguagem própria, de nível de tipo, com suas regras de tipos condicionais, mapeados e inferência, separada da linguagem de valores que roda em tempo de execução. Você programa em dois idiomas que não se misturam, e os tipos são apagados antes de o código rodar. Em Zig é o oposto: não há um segundo idioma. A metaprogramação é escrita com if, for e funções comuns, e o resultado não é apagado, é gerado. O padrão em Zig é sempre o mesmo: expor o mecanismo em vez de escondê-lo.

Conceitos: comptime, tipo como valor, monomorfização, genérico como função, reflexão, geração de código, laço desenrolado, sem apagamento de tipo Linguagem: comptime, type, @This, @TypeOf, @typeInfo, @field, inline for, anytype Contraste: macros, templates de C++, generics de TypeScript, apagamento de tipo