Pular para o conteúdo

Racional · ensaio 08

Comptime e reflexão

rationale.md · 82 linhas · 3 min de leitura

A redefinição: metaprogramação é biblioteca, não mágica. Comptime é código puro rodando no compilador; reflexão não tem builtin nenhum.

Gerar código a partir de tipos (serializar, hashear, derivar) precisa de alguma forma de metaprogramação. As escolhas históricas: macros (C, substituição de texto), macros procedurais (Rust, uma segunda linguagem), ou um punhado de builtins (@sizeOf, @typeName, em Zig). Cada uma adiciona um modo de pensar separado da linguagem.

Comptime (estilo Zig) é simplesmente “avalie isto em compilação”: aplica-se a binding, bloco, loop, match e fn. type é um valor de primeira classe: um generic é uma função que recebe e devolve type, executada pelo compilador. Não há linguagem separada: é o mesmo código, rodando mais cedo.

Reflexão tem zero builtins. Não existe @type_name/@size_of no escopo. Tudo mora no pacote compiler: reflect(T) (a estrutura do tipo, casada com comptime match reflect(T).kind), fail(msg) (aborta a compilação com mensagem). É assim que serialize/hash/json se auto-derivam, sem você escrever nada, e mesmo assim nada disso é builtin.

E o comptime match reflect(T).kind é exaustivo: o compilador exige que você trate todos os kinds (ou um _/fail). Um tipo não-serializável não compila: o braço Ptr | ManyPtr | Channel | Function | RawPtr => fail(...) recusa na cara, em vez de gerar lixo.

A regra-raiz é o não-envenenamento levado ao limite: o escopo global não carrega função nenhuma. Cada builtin seria um envenenador do namespace: conflita com nomes de usuário, polui o global, e te faz decorar “é @size_of ou size_of?”. Mover tudo para reflect(...)/fail(...) mantém o namespace limpo: metaprogramação é uma biblioteca que você importa, não mágica embutida.

Por que comptime é seguro de existir? Porque o que roda em compilação é quase forçado pela arquitetura a ser o núcleo puro e determinístico da linguagem, sem IO, sem processos, sem channels, sem syscalls. Comptime nunca lê um valor de runtime. Isso é o que deixa o compilador otimizar agressivamente e garante reprodutibilidade, e é por isso que o escopo transient existe (mut/lambdas são expressões transient, presas ao seu lugar, sem vazar do comptime para o runtime).

A exaustividade do match reflect.kind não é detalhe: é a aplicação do mesmo princípio do match de domínio fechado (cap. 07) à derivação. E é uma lição aprendida na marra: a falta de um braço (Enum faltando, depois String faltando) mordeu o design mais de uma vez, e a regra “todo match reflect.kind cobre o conjunto inteiro” virou checagem permanente.

  • Macros (C). Substituição de texto: quebra léxico e estrutura, e você depura a macro como uma linguagem à parte. Comptime é execução semântica real, não texto.
  • Builtins espalhados (@sizeOf/@typeName, Zig). Cada um polui o global e vira “qual builtin é qual?”. Tudo sob reflect(...) é um namespace, não um vocabulário a memorizar.
  • Traits/derives implícitos (Rust). Sem match exaustivo, você descobre que um tipo “quase” satisfaz três camadas de compilação abaixo, com erro confuso. A derivação por reflect + match exaustivo dá o erro no boundary, claro.
  • Deixar valores de comptime vazarem para runtime. Quebraria o determinismo e cegaria as otimizações; o escopo transient impede.

O macro C que não expandiu direito por um parêntese e você debuga às cegas. O programador novo de Zig parado em “qual é a função de reflexão mesmo?”, porque são vinte builtins. O derive de Rust que falha fundo na instanciação com uma mensagem que não aponta para o seu código. Cada uma é metaprogramação que virou um modo de pensar separado, com suas próprias regras e suas próprias surpresas.

Metaprogramação é biblioteca, não mágica. Comptime é o seu código rodando no compilador, puro e determinístico; reflect(T) pergunta “que tipo é este?” sem nenhum builtin; o match sobre o kind é exaustivo, então o compilador te força a tratar tudo.

Zero builtins custa verbosidade: você sempre escreve reflect(T).kind em vez de um @kind curto. E comptime ser puro (sem IO, sem ler runtime) limita o que ele pode fazer, em troca de reprodutibilidade total. A aposta é a de sempre: um pouco mais de texto, em nome de um namespace limpo e de uma fronteira nítida entre o que roda na build e o que roda no programa.

Próximo: 09 · Strings