Pular para o conteúdo

O livro · 10

Comptime e reflexão

book.md · 97 linhas · 3 min de leitura

Objetivo: rodar lógica em compilação, parametrizar tipos (generics), inspecionar tipos com reflect(T), e firmar contratos com assert/assume/@requires.

comptime é o modificador “avalie isto em compilação”. Ele se aplica a binding, bloco, loop, match e fn:

const SIZE := 64 // const = comptime let
comptime {
assert (SIZE <= 1024) // checado na build
}

type é um valor de compilação: tipos são valores de primeira classe. Um generic é só uma função que recebe/devolve type, executada pelo compilador.

decl List[T] { items: [*]T; len: usize; cap: usize } // açúcar de uma comptime fn que devolve type
fn repeat[n: int](s: string) -> string // n: compilação; s: runtime
fn zeros[N: usize]() -> [N]int // N dimensiona o array em compilação

Cada valor distinto em [...] gera uma função especializada, ou seja, monomorfização.

Constraints são declaradas, com régua estrita de símbolo: + significa implementa interface, : é whitelist (pertence a um conjunto):

fn serialize[T + Serializable](v: T) -> []byte // capability
fn encode[T: {i8, u16, bool}](v: T) -> []byte // whitelist
fn proc[T: {i8, u16} + Serializable](v: T) -> []byte // combina

O erro estoura no boundary da instanciação, limpo, e não três camadas fundo como no duck typing do Zig.

Duas formas, escolha por necessidade:

  • T + Trait / HKT gera monomorfização (estática; necessária quando o retorno depende de T, ex. map[A,B](xs: C[A]) -> C[B]).
  • valor de interface gera vtable (erased), capítulo 11.

reflect(T) expõe a estrutura de um tipo ao código comptime: é o que auto-deriva serialize/hash sem você escrever nada. A forma do tipo é um variant-set, casado com comptime match exaustivo:

use compiler.{reflect, fail}
fn serialize[T](v: T, out: mut Buffer) {
comptime match reflect(T).kind {
Struct(s) => comptime loop f in s.fields { serialize(f.get(v), out) } // f.get lê o campo
Tuple(parts) => comptime loop p in parts { serialize(p.get(v), out) } // posicional
Enum(e) => { put_varint(out, e.tag(v)); /* + payload da variante ativa */ }
Slice(_) => { put_varint(out, v.len); loop x in v { serialize(x, out) } } // length-prefix + N
Array(_, _) => loop x in v { serialize(x, out) } // n vem do tipo
Optional(_) => { out.write_byte(v.is_present() as byte); if v.is_present() { serialize(v.or_panic(), out) } }
String => { put_varint(out, v.bytes.len); out.write(v.bytes) } // contagem + bytes UTF-8
Int(_) | Float(_) | Bool => out.write(v.to_bytes())
Ptr(_) | ManyPtr(_) | Channel(_) | Function(_) | RawPtr
=> fail("não serializa: endereço não cruza a fronteira") // fail, do compiler
_ => fail("kind não serializável") // nada de valor cai aqui, todos os 10 kinds acima
}
}

Na prática você nunca vê reflect: escreve [T + Serializable] e a derivação roda por baixo. Quem desce a reflect(T) é autor de lib (encoding/json/hash).

A contenção de perigo (capítulo 13) tem dois níveis. Contrato de fronteira (@requires / pós-condição) torna uma função segura de chamar: o compilador checa em cada chamada (estático onde prova, panic onde não):

@requires(i < buf.len)
fn at(buf: Buffer, i: usize) -> u8 == buf.bytes[i] // pós-condição tersa no tipo de retorno
{ return unsafe raw_read(buf.ptr + i) assume "ptr válido: invariante de Buffer + bounds do @requires" }

Abate de operação no corpo: assert (cond) checa (panic se falsa); assume "razão" confia (sem check, sem licença pra UB). Duas keywords, uma intenção cada.

Próximo: 11 · Interfaces