zig-introduccion-comptime

Comptime en Zig: ejecutar código durante la compilación

  • 4 min

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 });
}
Copied!

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]});
}
Copied!

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);
    }
}
Copied!

El código generado por el compilador será equivalente a escribir a mano:

funcionCritica(10);
funcionCritica(20);
funcionCritica(30);
Copied!

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
#endif
Copied!

En 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", .{});
    }
}
Copied!

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; 
}
Copied!

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.