comptime es la capacidad de ejecutar código Zig durante la compilación. En la mayoría de lenguajes compilados hay una barrera clara entre “el momento de compilar” y “el momento de ejecutar”; Zig permite cruzarla de forma controlada.
- Compilación: Traduce tu código a binario. (Ocurre en tu PC).
- Ejecución: La CPU procesa los datos. (Ocurre en el PC del usuario).
En C++ la metaprogramación suele apoyarse en templates y en Rust, en macros. Ambos mecanismos tienen reglas distintas a las del código ordinario.
En Zig, el lenguaje de metaprogramación es el propio Zig.
Zig permite ejecutar código durante la compilación mediante comptime. Podemos precalcular datos, validar tipos y generar código sin introducir otro lenguaje dentro del lenguaje.
La palabra clave comptime
Cuando marcamos una variable o un bloque con comptime, le estamos ordenando al compilador: “Calcula esto AHORA y guarda solo el resultado final en el binario”.
Veamos un ejemplo clásico: calcular un factorial.
const std = @import("std");
fn factorial(n: u64) u64 {
if (n == 0) return 1;
return n * factorial(n - 1);
}
fn comparar(runtime_n: u64) void {
// El argumento solo se conoce al ejecutar el programa.
const x = factorial(runtime_n);
// El compilador debe evaluar esta llamada y obtiene 120.
const y = comptime factorial(5);
std.debug.print("runtime={d}, comptime={d}\n", .{ x, y });
}La misma función sirve en ambos contextos. Cuando todos los datos se conocen al compilar, el cálculo no tiene que repetirse durante la ejecución.
Generación de tablas
En desarrollo de juegos, sistemas embebidos o criptografía, a menudo usamos tablas precalculadas (por ejemplo, una tabla de senos/cosenos o valores CRC) para evitar cálculos matemáticos lentos en tiempo real.
En C, tendrías que escribir un script en Python que genere un archivo .c con un array gigante.
En Zig, lo haces en el mismo archivo.
const std = @import("std");
// Función que genera un array de cuadrados
fn generarTabla(comptime N: usize) [N]u32 {
var tabla: [N]u32 = undefined;
for (0..N) |i| {
tabla[i] = @intCast(i * i);
}
return tabla;
}
// El inicializador de esta constante global se evalúa al compilar.
const cuadrados = generarTabla(10);
pub fn main() void {
// Acceso instantáneo, sin cálculo
std.debug.print("5 al cuadrado es {d}\n", .{cuadrados[5]});
}Desenrollado de bucles con inline for
Un bucle inline se desenrolla y hace que sus capturas sean conocidas en tiempo de compilación.
Zig nos ofrece inline for (y inline while).
const items = [_]u8{ 10, 20, 30 };
pub fn main() void {
// El compilador "copia y pega" el cuerpo del bucle para cada elemento
inline for (items) |item| {
funcionCritica(item);
}
}El código generado por el compilador será equivalente a escribir a mano:
funcionCritica(10);
funcionCritica(20);
funcionCritica(30);Esto es necesario cuando iteramos sobre tipos o metadatos de tipos. Para usarlo únicamente como optimización conviene tener una medición que demuestre la mejora, porque desenrollar también puede aumentar el tamaño del binario.
Compilación condicional
En C, usamos el preprocesador para compilar código diferente según el sistema operativo:
#ifdef _WIN32
// Código Windows
#else
// Código Linux
#endifEn Zig usamos un if normal cuya condición se conoce en comptime. Solo se analiza semánticamente la rama elegida, lo que permite incluir código específico de cada objetivo.
const builtin = @import("builtin");
fn limpiarPantalla() void {
if (builtin.target.os.tag == .windows) {
// Este código solo se compila si estamos en Windows
llamarApiWindows();
} else {
// Este código solo se compila en Linux/Mac
std.debug.print("\x1b[2J", .{});
}
}Esto hace que el código multiplataforma sea mucho más fácil de leer y mantener, ya que sigue la misma sintaxis que el resto del programa.
comptime_int: enteros de precisión arbitraria
Una curiosidad de Zig es que, durante el tiempo de compilación, los enteros no tienen límite de tamaño (son de precisión arbitraria).
pub fn main() void {
// Este número no cabe en un u64
const gigante = comptime 1000000000000000000000000 * 500;
// Pero si el resultado final cabe en el tipo destino, funciona.
const resultado: u128 = gigante;
}Esto permite realizar cálculos matemáticos constantes muy complejos sin preocuparse por desbordamientos intermedios, siempre que el resultado final quepa en el tipo de runtime.