1 puntos por GN⁺ 2025-11-08 | 1 comentarios | Compartir por WhatsApp
  • El compilador de Zig, que incluye de forma nativa compilación de código C y compilación cruzada, es el lenguaje más sorprendente que el autor ha conocido en 45 años de experiencia
  • Con funciones únicas como ejecución en tiempo de compilación, variables de tamaño de bits arbitrario y entorno de bloques de prueba, va más allá de ser un simple reemplazo de C/C++ y ofrece una forma completamente nueva de programar
  • Con una sintaxis concisa y clara, como declaración de variables mediante inferencia de tipos, structs anónimos y break con etiquetas, se puede aprender rápidamente
  • Con pruebas independientes de módulos mediante bloques de prueba y la función integrada @breakpoint, permite depurar código optimizado
  • Con soporte para programación de bajo nivel usando campos de bits y operaciones bit a bit, logra eficiencia y robustez al mismo tiempo, integrando en un lenguaje compilado ventajas propias de los lenguajes interpretados

Prólogo

  • En 45 años de experiencia, no hubo ningún lenguaje tan sorprendente como Zig
    • Zig no es simplemente un lenguaje nuevo, sino una herramienta que cambia de raíz la forma de programar
  • Verlo solo como un reemplazo de C o C++ es una gran subestimación
  • El objetivo de este texto es presentar las funciones simples pero atractivas de Zig y ayudar a que los programadores puedan empezar rápido
  • Existen muchas más funciones que influyen en la adopción de Zig en la industria

El compilador de Zig

  • Incluye por defecto compilación de código C y compilación cruzada sin configuración adicional, lo que tiene un gran impacto en la industria
  • La instalación consiste en descargar el compilador correspondiente al procesador/SO desde la página de descargas de Zinglang, descomprimirlo y copiarlo al directorio deseado
    • En Windows 10, se copia el archivo zip x86_64 en "Program Files" y se cambia el nombre del directorio raíz a "zig-windows-x86_64" para no tener que modificar la variable de entorno Path al actualizar la versión
    • Tras agregar la ruta del directorio raíz a la variable de entorno Path, el compilador puede usarse en modo CLI
  • Para compilar el programa "Hello World!", se recomienda consultar la sección "Getting Started" del sitio oficial

Conceptos y comandos principales

Declaración de variables

  • La declaración de variables se compone de una primera parte con accesibilidad (pub u omitida), var/const y nombre de la variable, una segunda parte con la declaración del tipo y una tercera con la inicialización
    • Solo la primera y la tercera parte son obligatorias, y el tipo puede inferirse a partir del valor de inicialización
    • Ejemplo: var sum : usize = 0;
  • Las variables declaradas sin pub solo son accesibles dentro del módulo (similar a las variables static en C)
  • No se recomienda declarar variables pub, y se aconseja minimizar las funciones pub para reducir el acoplamiento y aumentar la cohesión

Structs, structs anónimos y bloques de prueba

  • Los literales de struct anónimo rodeados por .{ y } se usan para inicializar elementos de otros structs o crear nuevos structs con sus elementos inicializados
  • .{ } es un literal de struct anónimo vacío
  • La forma struct { } es una declaración de struct
  • Los bloques de prueba permiten compilar y ejecutar pruebas sin necesidad de un ejecutable

Campos de bits

  • Los campos de bits se declaran como campos con tipos de tamaño específico dentro de un packed struct
  • Los punteros pueden apuntar a un campo de bits específico

Bucle for

  • La sintaxis de Zig es más clara que la de C, pero usa un intervalo abierto [0..9) en lugar de [0..8]
  • La declaración de tipo, inicialización, prueba e incremento de la variable de bucle i se manejan automáticamente

Arreglos

  • [_] define un arreglo cuyo tamaño no se conoce explícitamente, seguido por el tipo de elemento y la inicialización
    • Ejemplo: var grid = [_]u8{0} ** 81; inicializa 81 elementos u8 con 0
    • El tamaño del arreglo se infiere a partir del argumento de repetición de la inicialización
  • En el entorno de prueba, se puede recorrer el arreglo y acumular sus elementos
  • Las variables declaradas entre | en un bucle for se asumen automáticamente del mismo tipo que los elementos del arreglo
  • usize es el entero sin signo natural de la plataforma (u64 en 64 bits, u32 en 32 bits)

Punteros de múltiples elementos

  • Para que un puntero a arreglo pueda usar aritmética de punteros, debe declararse explícitamente como puntero de múltiples elementos, por ejemplo [*]const i32
  • Aunque el arreglo sea const, el puntero puede declararse como var

Desreferenciación de punteros

  • Un puntero al que se le asigna la dirección de una posición individual de un arreglo no puede actualizarse con aritmética de punteros
  • La desreferenciación de punteros se hace con ptr.*

Break con etiqueta

  • Es posible realizar diversas tareas en tiempo de compilación, como inicializar arreglos
  • El break con etiqueta pone : después del nombre del bloque y devuelve un valor desde el bloque con break
    • Ejemplo: break :init m;
  • 0.. es un rango infinito que comienza en 0
  • En un bucle for, las variables se inicializan e incrementan automáticamente, y el bucle termina después de procesar la última posición del arreglo
  • Un arreglo puede no inicializarse explícitamente con undefined

Funciones en Zig

  • Las funciones se declaran con fn y por defecto son static (solo se usan dentro del archivo)
    • Si se declaran como pub fn, pueden importarse desde otros archivos
  • Las funciones pueden ser "inlined"
  • En los punteros a función, const va al frente y luego sigue el prototipo de la función

Programación orientada a objetos en Zig

  • Los structs pueden tener funciones
  • En el ejemplo de la pila, pueden almacenarse hasta 81 elementos del tipo StkNode
  • Los operadores ++ y -- no existen en Zig; se usan += y -=
  • El puntero de pila es un entero usado como índice del arreglo stk
  • El puntero self no se pasa explícitamente como parámetro, sino que se asume indirectamente como el puntero a la instancia de pila sobre la que se llama la función
    • En una llamada como stack.pop(), self es un puntero a stack (similar a this en Java/C++)
  • La función init() es el constructor de la pila
  • Las funciones pop y push son "inlined"

Compilar y ejecutar programas Zig

Compilar un ejecutable

  • Para generar un ejecutable se necesita una función main que represente el punto de entrada del programa
  • En programas simples, la función main puede incluirse en el mismo archivo
  • Para depurar módulos de forma independiente, se puede insertar una función main al final del archivo y comentarla después de terminar la depuración
  • Comando de compilación: zig build-exe -O ReleaseFast program.zig

Ejecutar bloques de prueba de un módulo

  • Es una de las mejores funciones de Zig, y se usa para pruebas y prototipado
  • Los bloques de prueba comienzan con test "message" { y terminan con }
    • "message" es la cadena que se muestra al ejecutar la prueba
  • Los bloques de prueba se ejecutan independientemente del ejecutable, y el ejecutable final no los ejecuta
  • Comando de prueba: zig test module.zig
  • El bloque de prueba de example.zig prueba las funciones set y print; set recibe una cadena decimal como parámetro y print imprime el encabezado "Input Grid" seguido del grid

Salida en Zig

  • La instrucción std.debug.print llama a la función print de debug.zig en la biblioteca estándar de Zig, std
  • El primer parámetro es una cadena de formato, y el segundo es un struct anónimo que contiene la lista de variables a mostrar
  • Si no hay formato, el struct está vacío
  • Por defecto se muestra en stderr
  • A diferencia de printf en C, Zig puede procesar en tiempo de compilación la cadena literal y la lista de variables

Depuración de ejecutables

  • Usar un depurador no es simple salvo en IDEs con depurador integrado (Eclipse, IntelliJ IDEA) o kits de desarrollo integrados (w64devkit)
  • Integrar símbolos hace crecer el código y exige compilar en modo Debug, lo que produce código ejecutable notablemente menos eficiente
  • Zig ofrece una solución práctica para evitar estos problemas

Función integrada @breakpoint

  • Insertar @breakpoint(); en el código fuente hace que, al ejecutarse bajo un depurador, el programa se detenga en ese punto
  • Es una función útil para depurar código Zig optimizado sin símbolos
  • Si se usa std.debug.print justo antes de @breakpoint(); para imprimir las variables que se quieren rastrear, se pueden verificar sus valores en ese instante
  • En el ejemplo debug_example.zig, se insertan código para imprimir grid y variables dentro de la función set, junto con @breakpoint();
  • Comando de compilación: zig build-exe debug_example.zig
  • Luego se invoca debug_example.exe con un depurador como gdb y se ejecuta el programa con el comando r
  • Con el comando c se continúa la ejecución mientras se sigue el contenido de grid y las variables
  • Si se presiona Enter repetidamente para continuar, se puede comprobar que los valores de grid coinciden con el bloque de prueba de example.zig

Programación de bajo nivel en Zig

Representación de la matriz

  • Los dígitos decimales se almacenan en la matriz como enteros estándar u8
  • El grid de entrada tiene formato de cadena, pero los caracteres ASCII se convierten internamente en enteros u8
  • El almacenamiento de números se organiza linealmente, fila por fila, en el arreglo grid de 81 posiciones: var grid = [_]u8{0} ** 81;
  • Para verificar la exactitud del grid, hace falta acceder a los elementos por fila y por columna
  • Se crea un arreglo de 9 punteros, cada uno apuntando al inicio de una fila
  • Se usa break con etiqueta para devolver un valor desde un bloque de código: break :fill9x9 m; inicializa matrix con m
  • Notación de acceso a elementos: element = matrix[i][j]

Representar dígitos decimales como bits

  • El concepto clave consiste en reemplazar el dígito decimal entero i por el entero code
    • i ∈ [1,9] → code = 2ⁱ⁻¹
    • i = 0 → code = 0
  • La única posición de bit en la que code tiene valor 1 es i-1 (cuando i está entre 1 y 9); en caso contrario, todos los bits son 0
  • Se muestra una tabla con los valores de code para cada dígito (1→1, 2→2, 3→4, ..., 9→256)

Cálculo de code en Zig

  • El valor code se calcula con el operador de desplazamiento a la izquierda solo cuando c no es 0: code = @as(u9,1) << (c-1);
  • En Zig, las constantes deben tener un tamaño adecuado para que la operación se compile y el resultado se asigne a una variable
  • code se declara como tipo u9 (el valor máximo 256 requiere al menos 9 bits)
  • Zig permite variables con tamaño de bits arbitrario
  • La función integrada @as convierte la constante 1 al tipo u9

Representación del grid con campos de bits

Grid de campos de bits por fila

  • El arreglo lines refleja todo el grid representando cada fila como un entero de 9 bits: var lines = [_]u9{0} ** 9;
  • Al acceder al arreglo con la fila i, se verifica con una operación bit a bit AND (&) si un número específico ya existe en esa fila: lines[i] & code
  • Si el resultado es 0, el número aún no está en la fila i; de lo contrario, está duplicado

Grid de campos de bits por columna

  • El arreglo columns refleja todo el grid representando cada columna como un entero de 9 bits: var columns = [_]u9{0} ** 9;
  • Al acceder al arreglo con la columna j, se verifica con una operación AND bit a bit si un número específico ya existe en esa columna: columns[j] & code
  • Si el resultado es 0, el número aún no está en la columna j; de lo contrario, está duplicado

Reglas del Sudoku

  • Al insertar un nuevo número en un grid de Sudoku vacío, no debe existir ya en toda la fila, columna y celda que contienen al nuevo elemento
  • Una celda es cada uno de los 9 grids de 3x3 separados por líneas gruesas
  • Cada elemento específico del grid 9x9 pertenece a una fila, una columna y una celda únicas
  • En el grid de ejemplo, la primera celda contiene 3, 5, 6, 8 y 9, y faltan 1, 2, 4 y 7
  • Los arreglos lines y columns se encargan de revisar duplicados en filas y columnas
  • Hace falta un nuevo arreglo para revisar duplicados en las celdas

Grid de campos de bits por celda

  • El arreglo cells refleja todo el grid representando cada celda como un entero de 9 bits: var cells = [_]u9{0} ** 9;
  • Es más fácil acceder a cells como una matriz 3x3
  • Se llena el arreglo cell de manera similar a como se hizo con la matriz 9x9
  • A partir de la fila y columna del elemento en el grid 9x9 original, hace falta determinar la fila y la columna en la matriz cell
  • Como la división entera es muy lenta, se usa el arreglo cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; para proporcionar el resultado de la división
  • Al acceder a la matriz con la fila i y la columna j del elemento en el grid 9x9, se verifica con una operación AND bit a bit si un número específico ya existe en la celda del elemento: cell[cindx[i]][cindx[j]] & code
  • Si el resultado es 0, el número aún no está en la celda; de lo contrario, está duplicado

Prueba de duplicación de elementos

  • La verificación de duplicados del elemento se completa combinando con OR bit a bit (|) todos los elementos previos de la misma fila, columna y celda, y luego aplicando AND bit a bit con el code del elemento
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {  
    unreachable;  
}  
  • Si el resultado es 0, el elemento aún no existe en la fila, columna o celda
  • Si el resultado no es 0, el programa se detiene ejecutando la instrucción unreachable
  • Es la forma más simple de representar explícitamente un error en tiempo de ejecución en Zig
  • El código real también imprime detalles sobre la ubicación del error
  • Ejemplo: si se reemplaza el 0 justo después del primer 8 de la cadena de entrada por 5, ocurre un error porque ya hay un 5 en la fila 3, columna 1

Actualización de estructuras de datos

  • En la función set, un bucle for doble interactúa fila por fila para copiar cada nuevo elemento de la cadena de entrada s al grid
    • La variable k mantiene el índice del nuevo carácter de entrada en la cadena s
  • Al restar '0', el carácter se convierte a u4 (variable c)
  • Si el nuevo elemento a insertar en el grid no es 0 (c != 0), el code calculado con la instrucción de desplazamiento a la izquierda se copia a cada grid espejo
    • Se aplica OR bit a bit (|=) con el grid espejo correspondiente:
lines[i] |= code;  
columns[j] |= code;  
cell[cindx[i]][cindx[j]] |= code;  
  • No hace falta probar explícitamente si c está entre 1 y 9, porque se producirá un desbordamiento al ejecutar la operación de desplazamiento
  • Ejemplo: si se reemplaza el 0 justo después del primer 8 de la cadena de entrada por :, se produce un error en tiempo de ejecución
  • Si ese mismo 0 se reemplaza por /, ocurre un error similar en tiempo de ejecución
  • El programa solo funciona cuando los valores están entre 1 y 9, es decir, cuando el grid de entrada contiene únicamente dígitos decimales
  • Como muchos grids de Sudoku en la web representan 0 con ., en la función set existe la línea if (s[k] == '.') c = 0;
  • Esto evita convenientemente la operación de desplazamiento, ya que el valor de c es 0

Prototipado y robustez

  • Los errores forzados de las dos secciones anteriores muestran una función importante de Zig
  • Una de ellas es la robustez de Zig: en el caso de la operación de desplazamiento, no se permite un comportamiento incorrecto y este se detecta en tiempo de ejecución
  • Aunque todo parece orientado a la eficiencia, es un caso típico en el que el rendimiento se intercambia por robustez
  • En C, si una operación de desplazamiento pierde bits, es problema del programador, y eso se traduce en mejor rendimiento de ciertas instrucciones de ensamblador
  • La otra función es la posibilidad de usar bloques de prueba para prototipado
  • Las posibilidades de aplicación son innumerables, y la aplicación mostrada es solo depurar una situación específica cuando ocurre un error
  • Solo estas funciones ya ofrecen capacidades sorprendentes, muy poco comunes en un lenguaje de programación, especialmente en un lenguaje compilado

Conclusión

  • Zig se compone de tres elementos clave: compatibilidad con C, compilación cruzada e instalación simple
  • Estas características muestran su potencial para convertirse en un nuevo estándar de los lenguajes de programación de sistemas
  • Muchas ventajas que antes solo se encontraban en lenguajes interpretados están migrando gradualmente a lenguajes compilados para ofrecer mejor rendimiento
  • En Zig, esa similitud con los lenguajes interpretados destaca especialmente por el concepto de ejecución en tiempo de compilación
  • Esto hace que Zig sea especialmente diferente y poderoso, aunque también más difícil de entender

1 comentarios

 
GN⁺ 2025-11-08
Opiniones de Hacker News
  • Este artículo al principio afirma que “Zig no es solo un lenguaje simple, sino una forma completamente nueva de programar”, pero en realidad casi no habla de funciones realmente propias de Zig
    La inferencia de tipos, las structs anónimas, labeled break y demás ya existían en otros lenguajes desde hace mucho
    Lo verdaderamente único es comptime, pero esa parte ni siquiera se menciona
    No es un concepto totalmente nuevo como los macros de Lisp, pero resulta interesante la forma en que Zig lo usa en lugar de genéricos
    Aun así, la afirmación del artículo se siente bastante exagerada

    • Rust también podría considerarse “una forma completamente nueva”
      Rust permite expresar con claridad el momento en que se ejecuta el código, y es impresionante su diseño tipo motor de consultas que recorre todo el espacio del código
    • El lenguaje D ya soportaba ejecución de funciones en tiempo de compilación desde 2007
      Ver la documentación de D
      Si es una const-expression, se ejecuta automáticamente
    • Ya no tiene sentido meter a C/C++ en un mismo saco
      Son lenguajes completamente distintos, como Java y Scala
    • comptime no es tanto un invento mágico como una versión moderna de la metaprogramación
      Zig es más limpio que las plantillas de C++, pero se siente más como una alternativa práctica que como algo revolucionario
      Personalmente, no entiendo el entusiasmo excesivo, igual que pasó con Rust
    • Al ver la frase “una forma completamente nueva” esperaba un paradigma nuevo como LISP o Prolog, pero en realidad no había nada de eso
      Leí toda la documentación de Zig y me desconcertó no encontrar nada especialmente sorprendente
  • El mayor problema de Zig es que no puedes adjuntar datos a los errores
    Los errores solo se transmiten por un canal secundario, lo que dificulta el debugging, y al final los desarrolladores terminan omitiendo los datos del error
    Ver este issue relacionado
    Con un código simple como AccessDenied es difícil saber cuál fue la causa

    • Leí este texto de matklad, y me pareció convincente su enfoque de separar los códigos de error de la información diagnóstica
      En la práctica, incluso usando objetos Error complejos, muchas veces hace falta un canal de diagnóstico aparte
    • En lenguajes de sistemas, adjuntar datos a un error no siempre es buena idea
      Por el overhead de rendimiento o por problemas del estado del sistema, según el caso puede ser más seguro resolverlo con binding diferido
      Zig tiene una filosofía que prioriza esta precisión y determinismo
    • En Zig también se está discutiendo la posibilidad de incluir información definida por el usuario en los stack traces de errores
      Ver este issue relacionado
      Pero lo que de verdad hace falta es logging estructurado y rastreo de contexto basado en la pila de llamadas
    • std.zon suele ponerse como buen ejemplo, y en la comunidad hay movimiento para reunir distintos patrones de manejo de errores e incorporarlos al estándar
    • El hecho de no poder adjuntar datos a los errores, de hecho, empuja a diseñar errores más claros
      Puede evitar que desarrolladores perezosos simplemente les peguen datos por encima sin pensar
  • Estoy de acuerdo con la idea de que la forma de desarrollar Zig en sí misma es una nueva manera de desarrollar un lenguaje
    Impresiona ese proceso de evolución lenta en el que revisan cuidadosamente las funciones y eliminan lo innecesario

    • Pero este tipo de enfoque también es común en Java, Rust y otros
      Me gustaría escuchar con más detalle qué tiene Zig de realmente distintivo
  • Me gusta que Zig se pueda instalar desde PyPI
    El paquete ziglang se puede instalar con pip install ziglang y usar de inmediato
    Incluso se puede compilar código C con uvx

    • Como con los wheels de Python se puede empaquetar software arbitrario, este tipo de instalación es posible
    • Pero este enfoque también se siente como una reinvención más incómoda que nix
    • Ojalá Nim tuviera también una opción de instalación así
    • Personalmente, me parece que micromamba o pixi son mejores formas de gestionar paquetes que pip/uv
    • Gracias a las herramientas de IA, ahora es mucho más fácil aprender cualquier lenguaje
  • Da un poco de pena que presenten como “innovación” de Zig funciones que ya existían en lenguajes como Ada, Object Pascal o Modula-2
    Es interesante cómo, al volver a empaquetarlas con sintaxis estilo C, ideas de hace 40 años parecen nuevas otra vez

  • La introducción del artículo estaba bien, pero después solo se queda en una enumeración de funciones de Zig
    Su sintaxis intuitiva y su flujo de control explícito (defer, etc.) sí resultan atractivos
    Gracias a comptime, tampoco hace falta aprender una sintaxis separada para macros

    • El verdadero encanto de Zig está en un diseño sin redundancias innecesarias
      Todo encaja de forma natural, y aunque lo uses por primera vez, se siente como una herramienta que ya conocías desde hace tiempo
    • También vale la pena leer este análisis de la sintaxis de Zig de matklad
  • La sintaxis for (0..9) de Zig es intuitiva, pero al ser un intervalo abierto, a veces confunde
    Como con range(0, 9) de Python, es fácil olvidar si incluye o no el último valor

    • Rust lo deja claro separando 0..9 y 0..=9
    • La consistencia de usar solo intervalos semiabiertos, como hace Zig, en realidad reduce errores
      El tamaño del intervalo se calcula simplemente por diferencia, y recorrerlo en reversa también se vuelve sencillo
    • Odin lo distingue de forma más explícita con 0..<5 (abierto) y 0...5 (cerrado)
  • No me gusta la regla de identificadores de Zig
    Se siente raro mezclar snake_case y camelCase
    Aun así, su sistema de build, su allocator, y la experiencia de compilación son excelentes
    Uso Rust principalmente, pero sigo teniendo curiosidad por Zig

    • A mí me pasa igual. Personalmente, no sigo la convención de nombres para funciones privadas
      Lo mismo con la regla de prefijos de las bibliotecas en C, también me resulta molesta
  • El atractivo de Zig no está en una sola función, sino en la acumulación de decisiones prácticas
    Decisiones que al principio parecían radicales se vuelven comprensibles conforme profundizas más
    Zig es un lenguaje que recompensa a los desarrolladores curiosos

    • Probé hacer un juego pequeño en Odin, y fue una experiencia realmente divertida
  • Una de las razones por las que Zig es bueno es que reconoce la realidad del código de sistemas de bajo nivel
    Muchos lenguajes ignoran estas cosas por motivos estéticos, pero Zig no lo hace

    • Si vas a la definición de la biblioteca estándar, puedes ver directamente cómo maneja casos especiales como Plan9 OS
      Ver la documentación de page_allocator
    • Aun así, hacen falta ejemplos concretos que respalden mejor este tipo de afirmaciones