2 puntos por GN⁺ 4 시간 전 | 2 comentarios | Compartir por WhatsApp
  • Para resolver fallas estructurales de la implementación existente, reescribieron 300.000 líneas de Rust en Zig y, tras 487 días, alcanzaron paridad funcional; apuntan a la primera versión numerada estable, 0.1.0, para más adelante este año
  • El nuevo compilador admite carga de código en caliente y compilación cruzada reproducible, interpolación de cadenas dentro de pattern matching, y elimina las asignaciones en heap en el ruteo HTTP; además redujo el tamaño wasm de Rocci Bird a 31 KB, menos de la mitad
  • Las razones clave para elegir Zig fueron los tiempos de compilación, los allocators granulares y el control de disposición de datos, un ecosistema adecuado para desarrollar compiladores y la verificación de código con memoria no segura; la compilación incremental de Zig 0.17.0 recompila unas 460.000 líneas en 35 ms
  • En la clasificación real de bugs, el compilador en Rust tuvo 21 casos de corrupción de memoria y el compilador en Zig tuvo 10, aunque la mayoría se debió a generación de código incorrecta; los errores de seguridad de memoria del propio compilador Zig fueron 2 use-after-free que corrompían nombres de archivo
  • Zig encajó bien con estructuras de datos sin punteros, deserialización sin parseo y reutilización del serializador de LLVM bitcode, pero la experiencia de desarrollo de Rust fue mejor en liberación automática de memoria en tests, polimorfismo, campos privados de structs, detección de código muerto y compatibilidad entre releases

Reescritura que alcanzó paridad funcional

  • El equipo del compilador de Roc reescribió durante alrededor de un año y medio 300.000 líneas de Rust en Zig y alcanzó paridad funcional con el compilador existente
  • La paridad funcional es un hito importante, pero no es una versión estable; la 0.1.0 del nuevo compilador está prevista para más adelante este año
  • Ahora se puede compilar el juego de WASM-4 de 2024 Rocci Bird con el nuevo compilador
    • El juego completo tiene menos de 1.000 líneas de código Roc
    • El código fuente actualizado es más conciso que el original
    • El binario wasm que genera roc build --opt=size pesa 31 KB, menos de la mitad del resultado del compilador anterior
  • En la página de inicio, con un compilador WebAssembly de 2,5 MB, se pueden escribir y ejecutar programas básicos de Roc en el navegador
  • La reescritura tomó 487 días, 476 días más que los 11 días que le llevó a Bun trasladar unas 500.000 líneas de Zig a Rust
    • Bun hizo un port directo, mientras que Roc fue más bien una reescritura que cambió de forma importante varias estructuras y funcionalidades del compilador
    • Por eso es difícil comparar directamente el tamaño o la duración de ambos codebases solo en función de Rust y Zig

Experiencia de desarrollo que ofrece el nuevo compilador

  • Carga de código en caliente y compilación cruzada

    • Después de ejecutar el servidor con roc server.roc, si se modifica el código, el nuevo código se aplica automáticamente a partir de la siguiente solicitud
    • Es un comportamiento común en lenguajes interpretados como Python, pero poco habitual en lenguajes compilados de alto rendimiento como Roc
    • Funciona de la misma manera no solo en servidores web, sino también en juegos 2D sencillos
    • Al desplegar, roc build server.roc genera un binario autónomo con optimizaciones de LLVM aplicadas
    • Con roc build --target=x64musl se puede compilar de forma cruzada un binario estático para Alpine Linux
    • Si se ingresan los mismos bytes de código fuente, cualquier sistema de build, incluido Mac, genera los mismos bytes de salida
  • Interpolación de cadenas dentro de pattern matching

    • Admite interpolación de cadenas como "/users/${id}" dentro de pattern matching
    • A diferencia del ruteo estilo Express, no parsea templates de cadenas en tiempo de ejecución, sino que el compilador lo procesa directamente
    • Puede hacer match a la vez sobre el método HTTP y la ruta, y también ramificar rutas anidadas o valores por defecto mediante patrones
    • El código completo de manejo de solicitudes HTTP del ejemplo verifica la seguridad de tipos en compilación y no realiza ninguna asignación en heap
    • Implementó ruteo de solicitudes HTTP sin asignaciones usando ejecución en tiempo de compilación de funciones puras, y la sintaxis se puede probar en el compilador WebAssembly de la página de inicio

Por qué lo reescribieron desde cero

  • A diferencia de Rust, C o Zig, Roc no es un lenguaje de sistemas y usa gestión automática de memoria basada en conteo de referencias
    • Evita las pausas de un garbage collector de rastreo
    • Aprovecha la optimización Perceus y las modificaciones oportunistas al estilo Koka
  • A diferencia de los lenguajes no sistémicos típicos, no asigna heap por cada captura de closure, sino que usa defuncionalización polimórfica mediante especialización de conjuntos lambda
  • La defuncionalización permite varias optimizaciones posteriores, como el inlining en lenguajes funcionales, pero hacer correcta la implementación existente resultó extremadamente difícil
  • Mediante el prototipo en OCaml de Ayaz Hafiz comprobaron que el problema era una falla estructural que atravesaba varias etapas del compilador y que, para resolverlo, había que reescribir la mayor parte del compilador
  • Otros colaboradores también planeaban reescribir varias partes por distintos motivos, así que pasaron a evaluar una reescritura completa en vez de un enfoque tipo barco de Teseo, reemplazando gradualmente casi todo
  • En proyectos de compiladores exitosos es común reescribir desde cero, por ejemplo para self-hosting; también existe el caso de la reescritura de TypeScript trasladada a Go, un lenguaje distinto del propio
  • Aunque la política del compilador de Roc es no hacer self-hosting, en esta ocasión consideraron que los beneficios de la reescritura superaban los costos conocidos

Criterios para elegir Zig y no volver a Rust

  • El equipo ya usaba Zig para varias funcionalidades básicas de la biblioteca estándar y los únicos lenguajes de sistemas que conocían lo suficiente eran Rust y Zig, así que solo evaluaron seriamente esos dos
  • Cada proyecto tiene un lenguaje más adecuado; seguir usando Rust en otros trabajos y elegir Zig para Roc no es contradictorio
  • Tiempos de compilación

    • Los builds de cargo en Rust tomaban mucho tiempo incluso con compilación incremental y seguían siendo una molestia importante a medida que crecía el codebase
    • Esperaban que al pasarse a Zig los builds fueran mucho más rápidos
  • Control de memoria y disposición de datos

    • En cada etapa de compilación usan distintos allocators, en particular arena allocators, y aprovechan ampliamente la disposición de array of structs (SoA)
    • El ecosistema de Rust en general asume un único allocator global, mientras que el ecosistema de Zig toma como base pasar allocators granulares y también ofrece soporte SoA de forma estándar
  • Alcance del ecosistema necesario

    • El ecosistema completo de Rust es más grande que el de Zig, pero en ambos ecosistemas no había muchos paquetes relacionados con las necesidades específicas de Roc
    • Había más funcionalidades de nicho necesarias del lado de Zig, como código que genera bitcode rápidamente en vez de envolver la biblioteca C++ de LLVM
  • Soporte para código con memoria no segura

    • Las 300.000 líneas del compilador existente en Rust tenían alrededor de 1.200 usos de unsafe
    • Como comparación, el propio compilador de Rust usa alrededor de 40.000 unsafe en 3,5 millones de líneas
    • En un compilador que genera código máquina, las operaciones de memoria no segura son una parte importante del trabajo
    • Rust sigue el modelo de aislar bloques unsafe poco frecuentes y verificarlos con Miri o Valgrind, pero en Roc unsafe no era poco frecuente
    • Zig ofrecía más funcionalidades para hacer que el código con memoria no segura funcionara correctamente, y el equipo quería el mayor apoyo precisamente en esa área

Seguridad de memoria obtenida sin borrow checker

  • Alcance de las comprobaciones de Rust y Zig

    • Según material de Microsoft de 2019, cerca del 70% de las vulnerabilidades tratadas cada año mediante actualizaciones de seguridad eran problemas de seguridad de memoria
    • Si se divide la clasificación de 2018 de ese material desde la perspectiva de Rust y Zig, queda así
    • El 83.6% corresponde a lecturas/escrituras fuera de rango, casts no seguros, lecturas sin inicializar, desbordamientos de pila y problemas no relacionados con seguridad de memoria; son categorías que no dependen de elegir Rust o Zig
    • El 16.4% corresponde a use-after-free, una categoría que puede detectarse con el borrow checker de Rust, ReleaseSafe de Zig o comprobaciones al estilo Fil-C
    • ReleaseSafe de Zig provoca un pánico en tiempo de ejecución si se usa memoria liberada
      • Sus comprobaciones son menos exhaustivas que el subconjunto seguro de Rust
      • Tiene costo de ejecución y el programa puede entrar en pánico
    • ReleaseFast omite las comprobaciones en producción, pero las mantiene en builds de depuración y pruebas
    • Si se probaran todas las rutas reales de ejecución, se podría obtener una seguridad similar a ReleaseSafe, pero ese nivel de cobertura de pruebas generalmente no es realista
  • Otros proyectos en Zig y el unsafe de Rust

    • TigerBeetle usa ReleaseSafe, y en una verificación Jepsen minuciosa se encontraron 2 bugs de seguridad, pero ninguno estaba relacionado con seguridad de memoria
    • Ghostty y el compilador de Zig, que usan ReleaseFast, tampoco tienen CVE causados por inseguridad de memoria en código Zig
    • Los programas en Rust también pueden tener huecos de seguridad de memoria a través de unsafe dentro de sus dependencias
    • Unsafe Rust tiene riesgos similares a Zig ReleaseFast, pero no cuenta con una función equivalente a las comprobaciones en ejecución de Zig que detectan problemas durante el desarrollo
    • Miri y Valgrind pueden ayudar, pero no muchos proyectos Rust los usan
    • En cambio, la cultura de usar unsafe rara vez y revisarlo con mayor rigor es lo que en la práctica ha construido la sólida reputación de Rust en seguridad de memoria
    • También hubo casos de vulnerabilidades relacionadas con unsafe en proyectos basados en Rust
      • Deno tuvo CVE de lectura fuera de rango y use-after-free
      • Rocket tuvo un CVE de use-after-free
      • Actix tuvo varios CVE de inseguridad de memoria durante una etapa en la que su uso de unsafe era anormalmente alto
    • Roc consideró que use-after-free no era un gran riesgo, porque la mayoría de las asignaciones ocurren en arenas con tiempos de vida simples, y juzgó que las comprobaciones adicionales de Zig eran más útiles para código inherentemente no seguro

Bugs de corrupción de memoria confirmados tras la reescritura

  • El resultado de clasificar el issue tracker de Roc con Claude Opus 4.8 fue el siguiente
Compilador Hubo corrupción de memoria Sin corrupción de memoria Total
Rust 21 2,575 2,596
Zig 10 421 431
  • Los 21 casos de corrupción de memoria del compilador Rust no fueron corrupción de la lógica interna del compilador
    • El borrow checker cumplió su función prevista
    • Fueron bugs de miscompilation en los que el código máquina generado incorrectamente corrompía memoria en el programa compilado
  • 8 de los 10 casos de corrupción de memoria del compilador Zig también fueron miscompilations
  • Los 2 restantes fueron use-after-free en el código de reporte de errores
    • En los mensajes de error de roc check y roc bundle, el nombre del archivo aparecía corrupto como el carácter de reemplazo U+FFFD
    • El borrow checker de Rust habría podido detectar ambos bugs
  • El efecto que habría tenido para los usuarios reales elegir otra herramienta fue el siguiente
Elección de herramienta Impacto real en seguridad de memoria
Zig ReleaseFast 2 bugs en los que el nombre de archivo no se renderizaba en algunos mensajes de error
Zig ReleaseSafe 2 bugs en los que algunos errores entraban en pánico y no podían renderizar el nombre de archivo
Rust borrow checker Habría prevenido ambos bugs
  • Considerando 18 meses, cientos de miles de líneas y cientos de reportes de bugs, la diferencia entre las tres opciones no fue sustancial para el proyecto
  • Bun maneja juntos valores de JavaScript con garbage collection por rastreo y valores gestionados manualmente, por lo que use-after-free, double-free y fugas por no liberar memoria tuvieron un peso importante
  • El compilador de Roc no interactúa con JavaScript ni con otros garbage collectors por rastreo, así que no sufre los mismos problemas de gestión de tiempos de vida
  • Roc necesita más herramientas para encontrar errores de memoria en el código de salida generado que errores de memoria internos del compilador, y lo segundo queda fuera del alcance del borrow checker

Resultados reales en tiempos de build

  • zig build --watch -fincremental actualmente recompila cambios en unas 35 ms sobre aproximadamente 450,000 a 460,000 líneas de código Zig
  • La versión estable Zig 0.16.0 tiene un bug que rompe -fincremental en la base de código de Roc
    • La corrección ya fue incorporada, pero para usarla habría que pasar a un prebuild de Zig 0.17.0 con cambios incompatibles
    • Como también habría que vendorizar las dependencias relacionadas y actualizarlas a 0.17.0, se decidió esperar al próximo release estable
  • Los resultados medidos en una desktop Intel y Ubuntu 26 fueron los siguientes
Compilador Roc Tamaño del código Build en frío Build incremental
Original basado en Rust 1.85.0 354K 32.4 s 10.0 s
Original basado en Rust 1.97.0 354K 25.4 s 3.4 s
Punto de paridad funcional con Zig 0.16.0 320K 39.6 s 8.6 s
Reescritura actual con Zig 0.17.0 464K 32.1 s 0.035 s
  • En el punto de paridad funcional, incluso los artefactos que casi no cambiaban se recompilaban siempre, pero ahora solo se generan cuando hace falta
    • Con este cambio, aunque el código creció alrededor de 50%, el build en frío se volvió más rápido
  • Al pasar de Rust 1.85.0 a 1.97.0, el build incremental bajó de 10 s a 3.4 s, una mejora de alrededor de dos tercios en 18 meses
  • Los 35 ms de Zig son aproximadamente una centésima parte de 3.4 s, y el resultado proviene de una base de código alrededor de 50% más grande que la del código medido en Rust
  • Actualmente -fincremental solo funciona en x86-64, y muchos colaboradores usan Mac basadas en ARM, por lo que todavía no se puede aprovechar por completo la ventaja en tiempos de build

Estructura sin punteros y deserialización sin parseo

  • La nueva caché en disco usa como formato en disco la misma disposición de memoria eficiente para la ejecución, como en enfoques usados en el compilador de Zig y en el desarrollo de juegos
  • Todas las estructuras de datos del compilador se representan con arreglos de índices de 32 bits en lugar de punteros, y en varios lugares se usa un formato de arreglos de structs
    • Reduce el uso de memoria y aumenta la velocidad de ejecución
    • Permite escribir las estructuras de datos directamente en disco sin convertirlas a un formato de serialización separado
  • Al deserializar, no se parsean los bytes del disco
    • Se leen los bytes en memoria
    • Se reubican algunas partes para que las estructuras de datos existentes apunten a los nuevos arreglos
    • Luego pueden usarse de inmediato
  • En la práctica, la velocidad queda limitada por la velocidad de I/O al leer bytes del disco a memoria
    • Si los datos están en la caché de disco del sistema operativo, los resultados de una compilación anterior se cargan aproximadamente a velocidad de memcpy
  • roc check guarda en disco, en la primera ejecución, resultados como el parseo y la verificación de tipos
    • Si el código fuente de entrada no cambió, en la segunda ejecución mueve directamente las estructuras de datos del disco a memoria
  • roc test también cachea los resultados de pruebas deterministas de funciones puras
  • La caché funciona por archivo, por lo que, si se cambia un archivo, solo se reprocesan ese archivo y los archivos dependientes
  • Este enfoque es posible porque todo el compilador usa índices en lugar de punteros; en estructuras típicas centradas en punteros, la deserialización sin parseo no es posible
  • Límites de seguridad de las estructuras basadas en índices

    • Así como un puntero puede apuntar a una dirección incorrecta, un índice también puede consultar el arreglo equivocado y leer bytes arbitrarios
    • El borrow checker de Rust gestiona la vida útil de los punteros, pero no verifica qué índice pertenece a qué arreglo
    • Si se conoce de antemano la cantidad de arreglos necesarios, compact_arena de Rust puede crear etiquetas de tipo con macros para impedir consultas al arreglo equivocado
    • Cuando la cantidad de arreglos varía según el número de módulos, como en Roc, esta técnica no se puede aplicar, por lo que compact_arena también marca SmallArena::new como unsafe
    • Crear una arena vacía en sí no genera peligro; el riesgo real está en la indexación de arreglos, que se realiza con mucha frecuencia
    • Safe Rust es eficaz bajo la suposición de que unsafe es pequeño y está aislado, pero en un caso como Roc, donde unsafe está muy extendido, esa suposición no se cumple

El ecosistema de Zig encajaba con Roc

  • Para Bun, el Drop de Rust, que ejecuta el código de limpieza una sola vez, era útil por la interoperabilidad entre JavaScript y la memoria manual
  • Roc, en cambio, quería usar arenas separadas por módulo y por etapa de compilación, por lo que los paquetes de Rust basados en un asignador global y Drop implícito resultaban incómodos
  • En el ecosistema de Zig es habitual que las API reciban asignadores de forma explícita, lo que encaja bien con el modelo de gestión de memoria de Roc
  • El ecosistema de Rust era más adecuado para la estructura que quería Bun, mientras que el de Zig era más adecuado para la estructura que quería Roc
  • Reutilización del serializador de bitcode de LLVM

    • LLVM es una dependencia central del optimizador de Roc y realiza optimizaciones adicionales después de las propias optimizaciones de Roc
    • LLVM rompe con frecuencia la compatibilidad de sus API principales, por lo que actualizar versiones requería bastante tiempo y costo
    • El formato de bitcode serializado de LLVM es estable y retrocompatible, así que usar un serializador propio permite independizarse de los cambios en la API de C++
    • Para ello se necesita un serializador de bitcode escrito a mano y separado de las bibliotecas C++ de LLVM
    • La implementación conocida existente estaba en el compilador de Zig, y el nuevo compilador de Roc reutiliza ese código Zig
    • La mayor fuente de dependencias que Roc obtiene del ecosistema de Zig no son tanto los paquetes generales, sino el propio compilador de Zig

Funciones de Rust que se extrañan en Zig

  • En la implementación de un compilador se necesita control explícito de la asignación, pero en las pruebas la asignación y liberación automáticas de Rust resultaban más cómodas
    • El asignador de pruebas de Zig detecta fugas de memoria y también puede detectar fugas en el código Roc compilado
    • En contrapartida, en cada prueba hay que escribir correctamente init y defer deinit; si uno solo está mal, la prueba falla por una fuga
  • Aunque comptime de Zig se superpone con el polimorfismo paramétrico y ad hoc, se extrañan ambas formas de polimorfismo
    • El trait Allocator de Rust puede recibir self
    • Implementaciones como ArenaAllocator de Zig deben recibir un puntero anyopaque y luego castearlo a su propio tipo
  • Al no haber campos privados en structs, no se puede impedir con un error de compilación el acceso directo a campos que no deberían tocarse
    • En una revisión de diferencias de código solo se ve el acceso al campo, no la documentación del struct original, así que hay que comprobarlo por separado cada vez
  • A veces se extraña la consistencia de Rust, donde funciones, variables y constantes usan todas snake_case
  • unsafe y el borrow checker también tenían costos, pero daban la tranquilidad de que ciertos problemas solo debían preocupar dentro de bloques unsafe
    • Esto no significa que deban agregarse las mismas funciones a Zig
  • En Zig era más común descubrir código muerto más tarde que en Rust
    • Las herramientas integradas de Zig y tidy.zig de TigerBeetle tampoco detectan parte del código muerto
    • El código muerto no se genera en el binario, por lo que no afecta a los usuarios, pero es una desventaja para el mantenimiento del codebase
  • En Rust, las actualizaciones de versiones menores y los cambios de edición fueron en su mayoría sencillos
  • Actualmente Zig no busca retrocompatibilidad, y como era una condición prevista no fue un gran problema, pero la experiencia de actualización simple de Rust fue mejor

Aspectos satisfactorios de Zig

  • Zig tiene el atractivo de un diseño sustractivo, que reduce herramientas conocidas, como en la programación funcional, para obtener otras propiedades
  • No tiene macros, y muchos problemas, incluido el polimorfismo paramétrico, se pueden resolver con comptime o con funciones comunes
  • Permite controlar con precisión la disposición de los datos
    • Usa tipos enteros que no son potencias de 2, como u7 y u5, sin manipulación de bits adicional
    • Ofrece soporte nativo para packed struct
    • Permite inlinear funciones en el sitio de llamada, no en el sitio de declaración
    • Funcionalidades que en Rust requerirían crates basados en macros se pueden usar sin dependencias adicionales
  • La cadena de herramientas de build de Zig encajó bien para generar binarios autónomos para Alpine Linux y WebAssembly
    • También maneja builds especiales que compilan los builtins equivalentes a la biblioteca estándar de Roc como blobs binarios opacos y los incluyen en el ejecutable final
    • Uber también usa la cadena de herramientas de Zig en su infraestructura de build sin usar el lenguaje Zig
  • En el manejo de errores de Zig, una asignación fallida en el heap también se trata como un error común de espacio de usuario
  • Roc usa una estrategia similar, donde los errores se acumulan de forma natural mediante tipos suma anónimos y payloads
  • Se prefiere el enfoque de Zig y Roc frente a anyhow, thiserror y el manejo básico basado en Result de Rust
  • Para la sintaxis de propagación de errores se prefiere el ? posfijo de Rust antes que el try de Zig, por lo que Roc también adoptó un operador ? posfijo
  • En conjunto, incluyendo las API de asignadores granulares y el código reutilizable para compiladores de alto rendimiento, hay mucha satisfacción con la elección de Zig para el proyecto

Próximos pasos de Roc

  • Se planea lanzar la 0.1.0 del nuevo compilador en la segunda mitad de este año, y será el primer lanzamiento numerado de Roc
  • Antes del lanzamiento también se pueden probar las builds nightly, pero actualmente aún quedan varios bugs, funciones incompletas y documentación incompleta
  • La Roc Programming Language Foundation es una organización sin fines de lucro 501(c)(3) de Estados Unidos, y las donaciones se usan principalmente para compensar a los contribuidores
  • El avance futuro del desarrollo y las preguntas se pueden seguir en Roc Zulip

2 comentarios

 
GN⁺ 3 시간 전
Opiniones en Lobste.rs
  • En números, los 35 ms de Zig son casi 100 veces más rápidos que los 3.4 segundos de Rust, y aunque el código también es alrededor de un 50% mayor, en el desarrollo real la diferencia parece algo exagerada.
    Si no recompilas el compilador cada minuto sino cada 10 minutos, el tiempo ahorrado es de unos 3 segundos, y 3.4 segundos ya es suficientemente rápido.
    A medida que crece la base de código, Rust puede volverse más lento, pero el tiempo de compilación en frío de hecho empeoró; además, si se considera que se cambia la laptop cada dos años y se suman las mejoras del propio compilador, es difícil afirmar que los tiempos de compilación de Rust seguirán aumentando a largo plazo.

    • Que aparezcan errores de compilación y resultados de pruebas invalidados apenas guardas, antes incluso de presionar la siguiente tecla o mover el cursor de edición, es una ventaja enorme.
      El modo check de Rust y el LSP son más rápidos que una recompilación completa, pero no llegan al mismo nivel.
    • Asumir que solo recompilas una vez cada 10 minutos subestima la diferencia.
      Con la combinación --watch -fincremental, recompilas cada vez que guardas y recibes feedback muy rápido y frecuente.
      Migré un proyecto personal de Rust a Zig, y también usé Rust durante años en el trabajo; la velocidad de compilación de Zig fue realmente un cambio que me permitió respirar.
  • 35 ms es sorprendentemente rápido.
    Solo volver a enlazar parece que tomaría más que eso; me da curiosidad qué está haciendo realmente el compilador.

    • Zig tiene un linker propio.
    • La clave es el enlace incremental y la forma de modificar el binario existente en el mismo lugar; recuerdo que en el compilador de Roc a esto le llamaban surgical linking.
      Si cambias la implementación de una función, basta con insertar el nuevo ensamblador en el binario existente, pero el inlining, los cambios de firma de funciones, las dependencias y las reubicaciones cuando falta espacio requieren análisis adicional.
    • El equipo de Zig se encargó de gran parte del trabajo de compilación incremental, y el nuevo linker ELF todavía no está terminado, pero fue diseñado con el objetivo de enlazar incrementalmente a nivel de funciones individuales y variables globales.
      Si modificas o agregas una función, busca un área libre suficientemente grande en la sección .text del ejecutable de salida y escribe ahí el nuevo código máquina.
      Si no hay espacio suficiente, puede que tenga que ampliar .text y mover otros datos, pero expande la sección de forma exponencial para amortizar el costo.
      Si hace falta, también agrega entradas a la tabla de símbolos y elementos de reubicación; como también deja espacio libre de antemano para ellos, al terminar solo queda cerrar el archivo.
      Estimando con base en los resultados de Tracy que vi recientemente, del total de 35 ms el linker usó apenas alrededor de 1 ms.
  • La cifra de que en 300 mil líneas de código Rust se usó unsafe unas 1,200 veces es mayor de lo que esperaba.
    Por ejemplo, Inko solo tiene 162 expresiones unsafe { ... } y 87 funciones unsafe expuestas al código generado mediante la ABI de C, y todo el código Rust tiene unas 88 mil líneas.
    Sin embargo, por la forma en que funciona Inkwell, todo el backend de LLVM es en la práctica una enorme zona unsafe, así que es difícil sacar conclusiones con una comparación simple.
    Yo solo marco como unsafe las funciones que realmente comprometen la seguridad de memoria, como manejar punteros directamente, pero otros desarrolladores también lo usan como una pista de que hay que hacer cierta operación antes para evitar un panic.

  • La cifra de que unsafe aparece 40 mil veces en la biblioteca estándar y el compilador de Rust es inexacta.
    Es el resultado de contar incluso la palabra en tests y comentarios, y la mayoría está en la biblioteca estándar; en el compilador en sí, incluso incluyendo comentarios y tests, hay menos de 2 mil apariciones.
    Durante mis contribuciones a rustc, la proporción de mis PR que contenían unsafe ni siquiera llegó al 1%, así de raro es el código inseguro dentro del compilador.
    Es natural que la biblioteca estándar, que implementa el runtime básico del que depende todo Rust, tenga mucho unsafe; ese tipo de código es inseguro en cualquier lenguaje, ya sea de forma explícita o implícita.
    Ese compilador es unas 10 veces más pequeño que rustc y usa una cantidad similar de unsafe, así que no diría que esté generalizado, pero sí se encuentra con frecuencia, y me da curiosidad por qué lo necesitan mucho más que rustc.

    • Nuestra base de código también mezcla compilador y biblioteca estándar, y parte de la biblioteca estándar se implementa como funciones intrínsecas del compilador, así que considero que esa métrica comparaba objetos similares.
      No analizamos por separado el unsafe relacionado con la biblioteca estándar en ambas bases de código, pero si hubiéramos implementado el nuevo compilador en Rust, esperamos que hubiera tenido más unsafe que rustc debido a la estructura que usa cachés e índices en lugar de punteros.
      No era nuestra intención menospreciar el trabajo del equipo de Rust; queríamos explicar nuestra elección y el estado del avance respetando los logros de otros proyectos.
  • Es un texto que evalúa de forma equilibrada las ventajas y desventajas de Rust y Zig sin ocultarlas, y resulta especialmente honesta la conclusión de que, incluso después de escribir cientos de miles de líneas durante 18 meses y resolver cientos de bugs, el resultado del proyecto probablemente no habría cambiado mucho si se hubiera elegido otro lenguaje entre las opciones
    Sin embargo, cuesta entender la explicación de que, en compiladores que generan código máquina como Roc o rustc, las operaciones no seguras de memoria sean una gran parte del trabajo
    También hay muchos compiladores escritos en OCaml o Haskell, y generar código máquina en sí consiste en armar bytes en un vector y escribirlos en un archivo, así que no hay motivo para que sea inseguro
    Lo entendería si se tratara de interpretación o compilación JIT, pero me intriga por qué sería necesario también en la compilación común

    • La expresión de que generar código máquina en sí es inseguro fue imprecisa, porque en realidad solo se trata de escribir bytes
      El riesgo aparece al ejecutar el código máquina generado, y como en muchos casos los compiladores reales generan código máquina y lo ejecutan al mismo tiempo, se lo describió como una gran parte del trabajo
      Además de la interpretación y el JIT, esto incluye evaluar código de usuario en tiempo de compilación, como const fn de Rust o expresiones que en Roc pueden elevarse al nivel superior, o ejecutar pruebas y luego inspeccionar la salida para decidir qué mostrarle al usuario
    • Parece querer decir que, para crear un compilador incremental muy rápido como el compilador de Zig, se necesita un lenguaje de bajo nivel y una cantidad considerable de operaciones no seguras
    • En el texto, los casos en que una compilación incorrecta provoca bugs de seguridad de memoria también se contabilizan como bugs de seguridad de memoria del compilador
      El criterio es que, ya sea que la memoria dañada esté en el proceso del compilador o en el programa generado, la causa de que el procesador se comporte mal son las instrucciones producidas por el compilador, y el lugar que debe corregirse también es el código del compilador
    • Roc ya usaba Zig para el runtime y la biblioteca estándar incluso cuando el compilador estaba escrito en Rust
      Componentes como el código de enlace externo y el garbage collector requieren mucho unsafe incluso en Rust, lo que neutraliza buena parte de las ventajas del Rust seguro
      Es posible escribir solo el compilador en un lenguaje seguro, pero el rendimiento es un problema, y Zig y el compilador de Roc usan ampliamente una estructura de arreglos (SoA) e índices de arreglos en lugar de punteros
      Si eso se implementa en Rust, se termina evitando el borrow checker y se pierden los beneficios de seguridad relacionados
      Las relaciones de vida útil en un compilador son sorprendentemente simples: en una etapa se pueden asignar, crear y modificar datos en un arena; pasarlos como solo lectura a la siguiente etapa; y luego descartar todo el arena
      Las partes más complejas son la compilación incremental y el enlazado, donde se lee estado desde el disco y se modifica un binario in situ; en ese proceso pueden surgir corrupción de estado, bugs, compilaciones incorrectas y problemas de memoria, pero eso es independiente de la seguridad de memoria del proceso del compilador en sí
      La seguridad y la corrección son conceptos mucho más amplios que la seguridad de memoria interna de un programa, especialmente cuando se trata de realizar de forma segura y correcta tareas difíciles de expresar en Rust
  • Como creador de compact_arena, me alegra que se lo haya presentado, pero se describió mal la forma en que esta biblioteca usa unsafe
    El objetivo de compact_arena es garantizar que new no pueda usarse mal desde Rust seguro, para hacer seguro el indexado
    El macro mk_arena puede llamarse desde código Rust seguro incluso dentro de bucles en los que no se conoce la cantidad necesaria de arenas

    • Quizá no entendí la explicación de que new se usa de forma segura, pero en la sección de seguridad de https://docs.rs/compact_arena/0.5.0/… se indica que la etiqueta que se pasa al constructor es la base del mecanismo de indexado, y que si ese valor se usa en otra arena, los índices de las dos arenas pueden mezclarse, lo que puede provocar accesos fuera de rango y comportamiento indefinido
      Mi punto es que el riesgo no surge de llamar a new() en sí, sino al indexar con el valor creado por new(), y considero que el lugar que debería auditarse y marcarse como unsafe es la llamada real de indexado
      Aunque es posible que haya entendido mal el diseño
  • Tengo entendido que Python requiere reiniciar el programa para reflejar código nuevo, y que la recarga en caliente necesita extensiones no estándar y escribir el código con cuidado
    La recarga en caliente es más común en entornos de desarrollo basados en imagen, como Lisp o Smalltalk, o en Erlang

    • Con importlib.reload() también es posible hacer recarga en caliente en Python, pero no sé qué tan extendido sea su uso
  • Me pregunto si se puede reducir el tiempo de compilación evitando usar cargo build
    No está claro si configurar Bazel sería más o menos trabajo que portar a Zig, y por supuesto el tiempo de compilación no fue la única razón para cambiar de lenguaje

 
GN⁺ 4 시간 전
Comentarios de Hacker News
  • En general el texto está bien, pero cuesta estar de acuerdo con la idea de que en compiladores que generan código máquina, como Roc o rustc, las operaciones inseguras de memoria sean una gran parte del trabajo
    Para cosas como parcheo de binarios en ejecución o recarga de código sí haría falta código inseguro, pero en el proceso normal de generar un ejecutable no hay razón para que la generación de código máquina en sí sea insegura
    Más bien, es más probable encontrar código inseguro en el runtime del lenguaje

    • De acuerdo. Generar código máquina no es más que escribir bytes, y la inseguridad potencial aparece al ejecutar ese código máquina
      Dicho eso, en la práctica muchos compiladores no solo generan código máquina sino que también lo ejecutan directamente, así que por eso se habló de que era “una gran parte del trabajo”, aunque un compilador no necesariamente tiene que hacer ambas cosas
      También se pensaba en cosas como el parcheo de binarios, la recarga de código, el runtime, así como evaluar código de usuario en tiempo de compilación, por ejemplo con const fn en Rust o expresiones que en Roc pueden elevarse al nivel superior, además de ejecutar tests y revisar su salida para decidir qué mostrar
    • Muchas veces se retuerce el hecho de que incluso los lenguajes con seguridad de memoria tienen bloques de código inseguro para concluir: “entonces, ¿de qué sirve la seguridad de memoria?”
      Es una lógica parecida a decir que, como igual puedes golpearte la cabeza usando cinturón de seguridad, entonces el cinturón solo estorba y no debería usarse
    • No es que la generación de código máquina requiera operaciones inseguras por naturaleza, pero aquí se reflejan prioridades distintas
      En código de rendimiento extremo cambian desde las estructuras de datos y los algoritmos hasta las estrategias de asignación de memoria, y TigerBeetle es conocido por asignar toda la memoria de una sola vez al arrancar
      Como el compilador de Roc también busca un compromiso parecido al de Zig, es natural encontrar muchos patrones en común
    • Decir que hacen falta operaciones inseguras para generar código máquina es claramente incorrecto. Un compilador puede trabajar solo con estructuras de datos completamente abstraídas, y si hay un lugar donde podría hacer más falta trabajo inseguro de memoria, sería algo más cercano al linker
    • Cuesta entender en qué parte de un compilador harían falta operaciones inseguras de memoria para crear código máquina de verdad
  • No se ve en qué se basa la afirmación de que ReleaseSafe detecta en runtime un use-after-free
    Revisé la documentación de Zig sobre verificaciones de seguridad de memoria en runtime, pero no encontré nada relacionado buscando use-after-free, UaF o safety-checked, y aunque uses DebugAllocator en builds de release, eso tampoco lo detecta de forma confiable
    El tema está resumido en https://landaire.net/memory-safety-by-default-is-non-negotia...

    • Desde la experiencia de haber escrito bastante código en Zig, incluso si esa función existiera, en mi código nunca ha funcionado bien
      Es una opinión formada escribiendo código directamente desde antes del auge de la IA, y aunque con la combinación de Zig y LLMs algo podría cambiar, mientras más leo el texto más afirmaciones raras veo, así que cuesta confiar en él
      Se lee menos como una evaluación técnica honesta y más como un texto escrito para justificar una discusión previa, aunque me gustan los textos y lenguajes raros y también me genera rechazo el exceso de entusiasmo por la IA, así que por ahora intento darle el beneficio de la duda
    • Entiendo que ReleaseSafe más bien agrega bounds checks y hace panic en código inalcanzable
      Zig no parece ofrecer seguridad temporal de memoria (temporal memory safety)
  • Es interesante que OCaml, aunque maduro, flexible y suficientemente expresivo como para validar prototipos, no haya sido elegido como lenguaje de implementación final
    También queda la duda de si el incremental build de Zig realmente será significativamente más rápido que dune, y aunque la compilación cruzada es una ventaja, eso tampoco se abordó en “por qué Zig”
    Da curiosidad si el control fino de memoria realmente es tan importante en un compilador, y en qué hito los mantenedores deciden pasarse a otro lenguaje, como ocurrió con Rust y WASM, que empezaron en OCaml

    • Rust dejó OCaml cuando decidió reescribirse en Rust. El texto también lo insinuaba como un momento común para reescribir un proyecto completo, y estoy de acuerdo con esa idea
    • Uno de los objetivos centrales de Roc es la velocidad de compilación. Supongo que OCaml quedó descartado porque no es un lenguaje de programación de sistemas
  • El incremental build de Zig claramente es una función decisiva, y se entiende elegir cambiar de lenguaje para obtener eso en el corto plazo
    Pero a mediano plazo esperaría que Rust también incorpore algo parecido en un futuro cercano
    Quiero velocidad, pero no quiero avanzar rápido para terminar disparándome en el pie, y estoy creando mi propio lenguaje que combine la seguridad de Rust, las capacidades de Zig y un runtime de Go sin recolección de basura

    • Los builds rápidos en Rust ya están en marcha en https://rust-lang.github.io/rust-project-goals/2026/roadmap-..., y la mayoría de los objetivos de esa página apuntan a este año
    • Es mucho más probable que los tiempos de compilación de Rust mejoren antes de que Zig se vuelva bastante más seguro
    • En vez de esperar a que el compilador de Rust se vuelva más rápido, me pregunto si no sería mejor intentar agregarle a Zig una especie de borrow checker
      Eso parece más viable, e incluso da la impresión de que podría implementarse en espacio de usuario
    • Me pregunto cuál sería la ventaja particular del runtime de Go, dejando fuera el recolector de basura
    • A veces uso tokens sobrantes de Codex en https://github.com/ityonemo/clr para intentar combinar la seguridad de Rust con las capacidades de Zig
  • Habría sido mucho más convincente si hubieran hecho una comparación científica real al elegir el lenguaje de implementación del compilador
    Parece que partieron de la suposición no verificada de que un compilador de alto rendimiento necesita un lenguaje de sistemas de bajo nivel (https://www.roc-lang.org/faq#self-hosted-compiler) y concluyeron que Zig era la única alternativa aparte de Rust
    El rendimiento de un compilador está dominado por los algoritmos; incluso los lenguajes administrados rápidos, con el mismo algoritmo, por lo general quedan dentro de un factor de dos en tiempo de ejecución, mientras que la brecha de rendimiento causada por diferencias algorítmicas no tiene ese límite
    Zig en sí mismo es un contraejemplo a la teoría de que escribir un compilador en un lenguaje de bajo nivel lo vuelve más rápido, y las aproximadamente 15 mil líneas por segundo de Roc no son rápidas. Hay material que indica que ya en 1998 un compilador de ML procesaba 3 mil líneas por segundo (https://flint.cs.yale.edu/cs421/case-for-ml.html)
    Podría ser más útil a futuro detener el trabajo actual del compilador y hacer un compilador autoalojado para un subconjunto pequeño de Roc que pueda hacerse en menos de 10 mil líneas
    Así, en vez de una implementación de 300 mil líneas, se podrían probar varias implementaciones a escala de 10 mil líneas y verificar si un lenguaje de bajo nivel realmente es necesario para cumplir los objetivos de rendimiento
    En el proceso de autoalojado aparecerían las funciones de Roc que de verdad importan, también se escribiría más código en Roc, y al mejorar las capacidades generales necesarias para el compilador también mejorarían las aplicaciones construidas encima

    • Exigir ciencia también suena tajante en esta evaluación, y tampoco hay mucha evidencia empírica
      El hecho de que en los años 90 se compilara ML rápidamente no basta para juzgar la velocidad de compilación actual de Roc. El diseño del lenguaje impone fuertes restricciones sobre los algoritmos necesarios, y el hardware moderno también es mucho más complejo
      Roc parece tener cierto grado de sobrecarga y algoritmos sofisticados para evitar asignar cierres en el heap, y esas exigencias podrían introducir una complejidad algorítmica inevitable
      Cuando se llega al límite de la optimización algorítmica, lo que queda es reducir factores constantes y, en particular, los lenguajes de alto nivel con manejo de memoria claramente ponen un piso a cuánto puede reducirse eso
      He visto casos en código real donde controlar directamente la disposición de la memoria mejoró el rendimiento en más de 10 veces, y en la industria de videojuegos ese tipo de trabajo incluso ocupa gran parte de una carrera profesional. La idea de que un solo algoritmo ingenioso hace desaparecer todos los problemas de rendimiento está lejos de la realidad
  • Las compilaciones de Rust son una de las causas de mayor desperdicio de espacio de almacenamiento en casi cualquier computadora, y al compilar varias librerías se acumulan decenas de GB muy rápido
    Se puede configurar una carpeta de compilación global para reutilizar dependencias entre proyectos, pero cualquier solución así sería mejor si se ofreciera como comportamiento predeterminado

    • Es una arquitectura que intercambia espacio en disco por compilaciones más rápidas, y en algunos casos usar todavía más espacio podría aumentar aún más la velocidad
      Aun así, hace falta recolección de basura del caché, y un nuevo trabajo de reorganización de artefactos intermedios de compilación para facilitar eso ya está en su etapa final
    • Es un contraste curioso si se piensa en cómo siempre se critica el tamaño de node_modules en el ecosistema de JavaScript
      Incluso en un proyecto de Tauri donde el código de backend es mucho más pequeño que el de frontend, los artefactos de compilación de Rust ocupan 9GB, mientras que node_modules apenas llega a 550MB
  • Entiendo Roc como un lenguaje de scripting para incrustarse en un ABI de C, y me da curiosidad cuál sería su caso de uso real
    Quisiera saber si apunta a competir con WASM en entornos de plugins que ofrecen una gran plataforma Roc, y por qué el desarrollador de una aplicación querría exponer una capa de Roc en vez de WASM, donde quien crea el plugin puede usar el lenguaje que quiera
    Si se trata de un lenguaje a nivel aplicación que usa una plataforma Roc pequeña, también me pregunto si busca competir con Gleam para código HTTP del lado del servidor y con Elm para código cliente

    • En especial, me gusta averiguar para qué aplicaciones se usa un lenguaje desconocido, sobre todo si es funcional, pero incluso leyendo roc-lang.org y el FAQ no logré entender con claridad en qué se usaría Roc
  • El tiempo de compilación es un factor gravemente subestimado. Mi mayor queja es esperar 10 minutos por una compilación de C++, porque eso rompe por completo el flujo de trabajo del desarrollo

    • En VS Code uso Rust-Analyzer para análisis sintáctico y autocompletado, pero como ejecuta el compilador para refrescar la información cada vez que guardo un archivo, se vuelve excesivamente lento
      El cambio que se siente al pasar de un archivo .rs a uno .ts es casi como si hubieras cambiado de computadora
  • No uso Zig directamente, pero me entusiasman varias posibilidades
    Me interesan especialmente nuevos juegos hechos en Zig, el software distribuido cuya promesa TigerBeetle ya mostró, y el campo de la robótica, que en lo personal me interesa mucho