Spinel: compilador nativo AOT para Ruby
(github.com/matz)- Convierte código Ruby en binarios nativos independientes y apunta a una ejecución con media geométrica de aproximadamente 11.6 veces más rápida que el
minirubyde CRuby actual, mediante inferencia de tipos a nivel de programa completo y generación de código C - El pipeline de compilación convierte Ruby en texto AST con un parser basado en Prism, luego un backend self-hosting realiza la inferencia de tipos y la generación de código C, y finalmente un compilador C estándar produce un binario standalone
- El backend del compilador tiene una arquitectura self-hosting escrita en Ruby, y tras el proceso de bootstrap se cumple
gen2.c == gen3.c, cerrando el ciclo de recompilarse a sí mismo - Incorpora optimizaciones en tiempo de compilación como aplanado de concatenación de strings, value-type promotion, loop-invariant length hoisting, static symbol interning y promoción automática a bigint, además de un motor regexp integrado, bigint y un runtime de un solo header para reducir dependencias externas
- No soporta
eval, metaprogramación, Thread ni manejo general de encodings, pero demuestra la practicidad de la compilación AOT para Ruby con una forma de distribución que se ejecuta sin Ruby y una gran diferencia de rendimiento en cargas de trabajo intensivas en cómputo
Cómo funciona
- El pipeline de compilación consiste en parsear archivos Ruby, serializarlos como archivos de texto AST, y luego pasar por inferencia de tipos y generación de código C para producir un binario nativo con un compilador C estándar
spinel_parseusa Prism y libprism para parsear Ruby, y cuando no hay un binario C disponible utiliza una ruta alternativa con CRuby y el gem Prismspinel_codegenfunciona como un binario nativo self-hosted y, a partir del AST, realiza inferencia de tipos + generación de código C- La etapa final compila el código fuente C junto con el header del runtime usando
cc -O2 -Ilib -lm, y el binario resultante se crea en formato standalone
Self-Hosting
- La cadena de bootstrap se cierra creando AST con
CRuby + spinel_parse.rb, generandogen1.cybin1conCRuby + spinel_codegen.rb, y luego usando otra vez el binario generado para creargen2.cygen3.c - Se indica que
gen2.c == gen3.c, confirmando que el bootstrap loop quedó cerrado - El backend
spinel_codegen.rbestá escrito en un subconjunto de Ruby que Spinel puede compilar por sí mismo- classes,
def,attr_accessor if/case/whileeach/map/select,yieldbegin/rescue- operaciones con String, Array, Hash y File I/O
- classes,
- El backend no incluye metaprogramming,
evalnirequire
Rendimiento y benchmarks
- Las pruebas están en 74 aprobadas y los benchmarks en 55 aprobados
- En 28 benchmarks, la media geométrica es de aprox. 11.6x más rápido que el
minirubyde CRuby actual - La comparación se hace contra un build reciente de CRuby
minirubysin gems empaquetados, y aun comparándolo con una referencia más rápida queruby3.2.3 del sistema, mantiene una clara ventaja en cargas intensivas en cómputo -
Rendimiento de cómputo
life: 20ms vs 1,733ms, 86.7x más rápidoackermann: 5ms vs 374ms, 74.8x más rápidomandelbrot: 25ms vs 1,453ms, 58.1x más rápido- versión recursiva de
fib: 17ms vs 581ms, 34.2x más rápido nqueens: 10ms vs 304ms, 30.4x más rápidotarai: 16ms vs 461ms, 28.8x más rápidotak: 22ms vs 532ms, 24.2x más rápidomatmul: 13ms vs 313ms, 24.1x más rápidosudoku: 6ms vs 102ms, 17.0x más rápidopartial_sums: 93ms vs 1,498ms, 16.1x más rápidofannkuch: 2ms vs 19ms, 9.5x más rápidosieve: 39ms vs 332ms, 8.5x más rápidofasta: 3ms vs 21ms, 7.0x más rápido
-
Estructuras de datos y GC
rbtree: 24ms vs 543ms, 22.6x más rápidosplay tree: 14ms vs 195ms, 13.9x más rápidohuffman: 6ms vs 59ms, 9.8x más rápidoso_lists: 76ms vs 410ms, 5.4x más rápidobinary_trees: 11ms vs 40ms, 3.6x más rápidolinked_list: 136ms vs 388ms, 2.9x más rápidogcbench: 1,845ms vs 3,641ms, 2.0x más rápido
-
Programas reales
json_parse: 39ms vs 394ms, 10.1x más rápido- cálculo de
bigint_fibde 1000 dígitos: 2ms vs 16ms, 8.0x más rápido ao_render: 417ms vs 3,334ms, 8.0x más rápidopidigits: 2ms vs 13ms, 6.5x más rápidostr_concat: 2ms vs 13ms, 6.5x más rápidotemplate engine: 152ms vs 936ms, 6.2x más rápidocsv_process: 234ms vs 860ms, 3.7x más rápidoio_wordcount: 33ms vs 97ms, 2.9x más rápido
Funciones de Ruby soportadas
- Como funciones Core, soporta classes, inheritance,
super, mixins coninclude,attr_accessor,Struct.new,alias, constantes de módulo y open classes sobre tipos integrados - En Control Flow soporta
if/elsif/else,unless,case/when, pattern matching concase/in,while,until,loop,for..in,break,next,return,catch/throwy&. - En Blocks soporta
yield,block_given?,&block,proc {},Proc.new,-> x { },method(:name), e incluye métodos con bloques comoeach,map,select,reduce,sort_by,times,uptoydownto - En Exceptions soporta
begin/rescue/ensure/retry,raisey clases de excepción definidas por el usuario - Los Types incluyen Integer, Float, String, Array, Hash, Range, Time, StringIO, File, Regexp, Bigint y Fiber
- Los valores polimórficos se manejan como tagged unions
- Para estructuras de datos autorreferenciales existen tipos de objeto anulables
T?
- Las Global Variables compilan
$namecomo variables C estáticas, y las incompatibilidades de tipo se detectan en tiempo de compilación - En I/O soporta
puts,print,printf,p,gets,ARGV,ENV[],File.read/write/open,system()y backticks
Strings, expresiones regulares, symbols, Bigint y Fiber
- Los Strings manejan tanto strings inmutables como mutables, y
<<promueve automáticamente a string mutablesp_Stringpara hacer append in-place en O(n) +, la interpolación,tr,ljust/rjust/centery métodos estándar funcionan sobre ambas representaciones de string- Comparaciones como
s[i] == "c"se optimizan para acceder directamente al arreglo de chars y se resuelven sin asignaciones - Concatenaciones como
a + b + c + dse aplanan en una sola llamada asp_str_concat4osp_str_concat_arr, reduciendo a N-1 asignaciones menos str.split(sep)dentro de un loop reutiliza el mismosp_StrArray, y encsv_processelimina 4 millones de asignaciones- Regexp usa un motor regexp NFA integrado sin dependencias externas
- Soporta
=~,$1-$9,match?,gsub,sub,scanysplit
- Soporta
- Bigint usa enteros de precisión arbitraria basados en mruby-bigint
- Se promueve automáticamente en patrones como multiplicaciones repetidas en loops, por ejemplo
q = q * k - Se enlaza como librería estática y solo se incluye cuando realmente se usa
- Se promueve automáticamente en patrones como multiplicaciones repetidas en loops, por ejemplo
- Fiber ofrece concurrencia cooperativa basada en
ucontext_t- Soporta
Fiber.new,Fiber#resume,Fiber.yieldy paso de valores - Las variables libres se capturan como celdas promovidas al heap
- Soporta
- Symbols se implementan con un tipo
sp_symseparado de String- Conserva
:a != "a" - Los literales symbol se internan en tiempo de compilación y se convierten en constantes
SPS_name String#to_symusa un pool dinámico solo cuando hace falta- Los hashes con claves symbol usan
sp_SymIntHash, almacenando claves enteras directas en lugar de strings, por lo que desaparecenstrcmpy la asignación dinámica de strings
- Conserva
Gestión de memoria y value types
- La gestión de memoria usa mark-and-sweep GC, con size-segregated free lists, marcado no recursivo y sticky mark bits
- Las clases pequeñas y simples se promueven automáticamente a value types y se ubican en la pila
- condición: hasta 8 campos escalares
- sin herencia
- sin modificaciones a través de parámetros
- Asignar un millón de veces una clase de 5 campos baja de 85ms a 2ms
- Los programas que solo usan value types no exportan en absoluto el runtime de GC
Optimizaciones
- Realiza varias optimizaciones en tiempo de compilación basadas en inferencia de tipos a nivel de programa completo
- Con Value-type promotion, las clases pequeñas e inmutables se convierten en objetos stack C struct y eliminan el overhead del GC
- Con Constant propagation, constantes literales simples como
N = 100se insertan directamente en los sitios de uso sin consultarcst_N - Con Loop-invariant length hoisting,
while i < arr.lengthcalcula la longitud una sola vez antes del loop- Si en el cuerpo se modifica el receptor, como con
arr.push, ese hoist se desactiva
- Si en el cuerpo se modifica el receptor, como con
- Method inlining agrega
static inlinea métodos cortos, no recursivos y de hasta 3 sentencias para inducir el inlining de gcc - String concat chain flattening reduce cadenas de concatenación a una sola llamada y evita crear strings intermedios
- Bigint auto-promotion promueve automáticamente a bigint patrones de suma autorreferencial o multiplicación repetida
- Bigint
to_susampz_get_strde mruby-bigint con estrategia divide-and-conquer O(n log²n) - Static symbol interning convierte
"literal".to_symen constantesSPS_<name>en tiempo de compilación, e incorpora el pool de runtime solo cuando hace falta interning dinámico - En
sub_range, strings con longitud previamente elevada usansp_str_sub_range_lenpara saltarse la llamada interna astrlen line.split(",")dentro de loops reutiliza elsp_StrArrayexistente- Dead-code elimination usa
-ffunction-sections -fdata-sectionsy--gc-sectionspara eliminar del binario final funciones del runtime que no se usan - Iterative inference early exit detiene de inmediato el loop de punto fijo si tres arreglos de firmas de parámetros, retornos e ivars dejan de cambiar
- La mayoría de los programas converge en 1 o 2 pasadas completas en lugar de 4
- El tiempo de bootstrap baja alrededor de 14%
- El byte walk de
parse_id_listreemplazas.split(",")por un recorrido manual cons.bytes[i]en el parser de listas de campos AST, llamado unas 120 mil veces durante la autocompilación, bajando las asignaciones por llamada de N+1 a 2 - El código C generado mantiene un warning-free build con el nivel normal de warnings, y el harness usa
-Werrorpara exponer regresiones de inmediato
Arquitectura
- La estructura del repositorio se divide en los siguientes elementos
spinel: script wrapper de un solo comando basado en POSIX shellspinel_parse.c: frontend C de 1,061 líneas desde libprism hasta AST textualspinel_codegen.rb: backend del compilador de 21,109 líneas desde AST hasta código Clib/sp_runtime.h: header de librería runtime de 581 líneaslib/sp_bigint.c: enteros de precisión arbitraria, 5,394 líneaslib/regexp/: motor regexp integrado, 1,759 líneastest/: 74 pruebas funcionalesbenchmark/: 55 benchmarksMakefile: automatización de build
- El runtime
lib/sp_runtime.hconcentra en un solo header el GC, la implementación de array/hash/string y otros soportes de runtime - El código C generado incluye este header, y el linker toma solo lo necesario desde
libspinel_rt.a- bigint
- regexp engine
- El parser tiene dos implementaciones
spinel_parse.cenlaza libprism directamente y funciona sin CRubyspinel_parse.rbes un fallback de CRuby que usa el gem Prism
- Ambos parsers generan la misma salida AST, y el wrapper
spinelprioriza el binario C cuando es posible require_relativese resuelve en tiempo de parseo y los archivos referenciados se insertan inline
Limitaciones
- No eval:
eval,instance_evalyclass_evalno están soportados - No metaprogramming:
send,method_missingydefine_methoddinámico no están soportados - No threads:
ThreadyMutexno están soportados; solo Fiber - No encoding: asume UTF-8 y ASCII
- No general lambda calculus: no maneja llamadas
[]ni-> x { }profundamente anidadas
Dependencias y modelo de ejecución
- Las dependencias de build son la librería C libprism y CRuby para el bootstrap inicial
- No hay dependencias en runtime, y los binarios generados solo requieren libc + libm
- Las expresiones regulares usan el motor integrado, por lo que no hace falta ninguna librería externa
- Bigint está integrado, pero solo se enlaza cuando realmente se usa
- Prism es el parser de Ruby usado por
spinel_parsemake depsdescarga el tarball del gem prism desde rubygems.org y extrae el código C envendor/prism- Si el gem prism ya está instalado, se detecta automáticamente
- También se puede indicar una ruta personalizada con
PRISM_DIR=/path/to/prism
- CRuby solo se necesita para el bootstrap inicial, y después de
maketodo el pipeline corre sin Ruby
Historial del proyecto
- Spinel fue implementado primero en C, con una escala de 18K lines, y permanece en la rama
c-version - Luego pasó por la rama
ruby-v1, reescrita en Ruby - El
masteractual es la versión reescrita otra vez en un subconjunto de Ruby capaz de self-hosting
Licencia
- Usa la licencia MIT
- Sigue el archivo LICENSE
1 comentarios
Comentarios en Hacker News
Si lo hizo Matz, entonces seguramente conoce bien incluso las limitaciones de la semántica de Ruby, así que inspira confianza
Mi tesis de maestría también fue sobre un compilador AOT de JS; sí funcionaba, pero las restricciones sobre los datos de entrada eran tan grandes que al final la abandoné
En ese tiempo los desarrolladores de JS no estaban tan acostumbrados a imponerse esas restricciones por su cuenta, y entradas intrínsecamente incognoscibles como
JSON.parseeran un obstáculoAhora, gracias a TypeScript, podría ser mucho más viable que en ese entonces
Incluso viendo el cálculo lambda general, los límites de la inferencia de tipos son claros, y se ven restricciones similares en los papers de Matt Might o en el trabajo de Shed-skin para Python
Me da curiosidad qué tan comunes son
eval,send,method_missingydefine_methoden código Ruby real, y también cómo suelen manejar entradas sin tipo, por ejemplo datos JSONParsear Ruby es casi más difícil que la propia traducción, así que usan Prism, y el resultado genera C
Implementar la semántica básica de Ruby en sí no es tan difícil
En cambio, yo sigo batallando con un viejo compilador AOT self-hosting hecho en Ruby puro, y por insistir en usar su propio parser tomé a propósito un camino mucho más difícil
Aprendí temprano que el primer 80% se puede hacer más o menos y aun así lograr que corra una buena parte del código Ruby; el verdadero “segundo 80%” difícil está concentrado en las cosas que Matz dejó fuera de este proyecto y de mruby, como la codificación y toda clase de funciones periféricas
Si soy honesto, Ruby también tiene varias funciones que nunca he visto una sola vez en código real, así que no me sorprendería que algunas quedaran deprecated
send,method_missingydefine_methodson muy comunesLas restricciones son parecidas a las de mruby, y aun bajo esas restricciones sí hay casos de uso
El soporte para
send,method_missingydefine_methodes relativamente fácilEn cambio, dar soporte a eval() es una pesadilla
Aun así, una gran parte del uso de
eval()en Ruby puede reducirse estáticamente a la versión con bloque de instance_eval, y en esos casos la compilación AOT se vuelve bastante sencillaPor ejemplo, si la cadena que entra a
eval()se puede conocer o descomponer estáticamente, hay bastante margen para resolverloDe hecho, mucho uso de
eval()es innecesario o se parece más a rodeos simples para introspección, así que puede tratarse con análisis estáticoEn mi compilador también pienso empezar por ahí si eso se vuelve el cuello de botella
La ingestión de JSON sin tipos probablemente también use mecanismos así
Si quitas eso, queda un lenguaje pequeño y fácil de leer que no es tan fuertemente tipado como Crystal, pero tampoco depende tanto de la metaprogramación como Ruby oficial
Por eso parece tener bastante potencial, pero al final habrá que ver con el tiempo
evalcon frecuenciaPodría no usarlo, pero para mí así es más ergonómico
eval,exec,define_methody también el patrón de crear clases nuevas conClass.newyStruct.newLa mayoría de esos usos se concentra en el arranque de la app o mientras se hace require de archivos, y en cierto sentido eso ya se parece a una etapa de compilación
Esto es lo que Matz acaba de presentar en RubyKaigi 2026
Es experimental, pero lo hizo en alrededor de un mes con ayuda de Claude, y la demo en vivo también salió bien
El nombre viene del nuevo gato de Matz, y el nombre del gato viene del gato de Card Captor Sakura, donde además hace pareja con un personaje llamado Ruby
En alguien como Matz, eso podría significar empujarlo de 100x a 500x
https://en.wikipedia.org/wiki/Spinel
Parece que el video todavía no está en vivo, y da la impresión de que los van subiendo uno por uno a este canal
https://www.youtube.com/@rubykaigi4884/videos
También da la impresión de que el nombre del proyecto se eligió de forma emocional
Sin duda es súper impresionante, pero parece imposible de mantener sin un agente de IA
spinel_codegen.rbtiene 21 mil líneas, y algunos métodos llegan a 15 niveles de anidaciónEl código de compiladores rara vez es bonito de por sí, pero incluso con ese estándar esto se ve muy difícil de mantener para una persona
Los compiladores tienen límites entre subsistemas muy claros y handoffs bien definidos entre etapas, así que en realidad están entre las cosas más fáciles de hacer de forma modular
El problema casi siempre es que primero se hace que funcione y luego ya no queda tiempo para refactorizar, y entonces el desorden sigue creciendo
spinel_codegen.rbestá casi al nivel de horror eldritchCuando uso Claude, a mí también siempre me sale este tipo de código espagueti, y pensé que tal vez yo estaba haciendo algo mal
Pero ver que incluso en un proyecto realmente interesante hecho por alguien a quien considero un programador de primer nivel la calidad del código es bastante mala en varias partes me hizo ver que no me pasa solo a mí
Por ejemplo,
infer_comparison_type()ni siquiera es el peor caso ni es ilegible, pero existe una implementación mucho más simple y clara y aun así Claude no llega ahíSi agrupas los operadores de comparación en un
Sety lo manejas coninclude?, queda más corto, más rápido, más legible y más fácil de mantenerPero Claude siempre termina cayendo en cadenas de if-return, e incluso da la impresión de que hasta los if-else le resultan ajenos
Mi codebase hecho con Claude también está lleno de ese patrón, así que ahora sé que no me pasa solo a mí
En cambio, otros archivos están bastante mejor, y en especial el directorio
libparece corresponder al directorioextdel repo principal de Ruby y tiene una calidad decenteLa API también está claramente influida por MRI Ruby, y aunque la implementación sea bastante distinta, parece que Matz guio el resultado para que se pareciera a parte de la API original y así quedara más ordenado
[1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
Si pasa las pruebas y los benchmarks, por ahora me doy por satisfecho
Aun así, sí me pregunto si un archivo gigante también es fácil de manejar para la IA
Yo intento limitar los archivos a menos de 300 líneas, y creo que el código fácil de entender para humanos también lo será para los agentes de programación
Dicen que las restricciones son estas
No eval:
eval,instance_eval,class_evalNo metaprogramming:
send,method_missing,define_method(dinámico)No threads:
Thread,Mutex(sí soporta Fiber)No encoding: asume UTF-8/ASCII
No general lambda calculus:
-> x { }profundamente anidado con llamadas[]En lo personal, asumir UTF-8/ASCII no me parece una restricción tan grave, pero el resto sí parece una limitación real para bastantes programas
Y volver a meter todo eso parece que requeriría bastante trabajo
Llevo mucho tiempo usando Ruby, y habiendo usado todas las funciones listadas, siento que al final de mi propia evolución justo terminé queriendo esta versión de Ruby simple
Es más simple y más fácil de entender, pero todavía conserva la estética propia de Ruby
Ahora, gracias a los LLM, la productividad para generar código es tan alta que ya no hace tanta falta reducir boilerplate con metaprogramación para mejorar la productividad del desarrollador como antes
Porque cada vez escribimos menos código directamente
La sintaxis es parecida y tiene un sistema de tipos estático, lo que lleva a código compilado más eficiente
evalhasta me parece mejor, pero que tampoco haya threads ni mutexes sí me decepcionaLa ausencia de
define_methodse entiende por su caso de usoPero
sendymethod_missingson comunes en librerías existentes, y tampoco parecería tan difícil implementarlos construyendo en compilación una tabla de lookup en memoriaAsí que no sé si lo dejaron fuera a propósito o si simplemente todavía no han llegado hasta ahí
Espero que sea lo segundo, pero al menos por ahora, por compatibilidad, parece difícil usarlo en producción
Fue reducir la cantidad de código que hay que leer
Esto está buenísimo, y yo llevaba mucho tiempo esperando un compilador AOT para Ruby
Sí da lástima que no haya fallback para
evalo metaprogramación, aunque parece que eligieron eso para concentrarse en un subconjunto pequeño y de alto rendimientoOjalá los gems construidos con este compilador AOT interactúen bien con MRI
Para empaquetar o bundlear Ruby estándar y gems todavía hacen falta tebako, kompo u ocran, y antes también existían proyectos como ruby-packer, traveling ruby y jruby warbler
Está bien tener una opción más, pero sigo esperando una versión definitiva con una mejor UX para desarrolladores
Porque llevaba demasiado tiempo sin actualizarse
Me pregunto por qué no threads
El scheduler de Ruby y la implementación subyacente con pthread parecerían funcionar bien también en el mundo de C, así que me pregunto si apuntan a zero dependency
Si no piensan agregarlo más adelante como extensión opcional, o si no es simplemente que todavía no lo implementan, esta decisión sí se siente un poco rara
Más bien sospecho que simplemente todavía no han llegado hasta ahí
El multithreading siempre ha sido muy difícil de hacer bien
Sorprende que lo hayan hecho en poco más de un mes
Se diga lo que se diga sobre la IA, en manos de desarrolladores con habilidad produce una aceleración enorme
Matz en cambio parece sentir que con
gem env|infoyfindya bastaSiendo algo hecho por Matz, me pregunto qué tan realista es que en el futuro pase a ser parte de Ruby core
Y si eso ocurriera, también me pregunto qué tanto amenazaría a Crystal
Esas características son prácticamente indispensables para compilar y mantener programas grandes
En cambio, esto es un subconjunto limitado de Ruby, así que la mayoría de los gems populares de Ruby no van a funcionar tal cual
Como subconjunto de lenguaje orientado a compilar a C, se parece más a PreScheme
En este punto no creo que ambos compitan directamente en el mismo espacio
Ruby completo casi con seguridad necesita JIT
[1]: https://prescheme.org/
Sería la revancha de herramientas como Rational Unified Process y Enterprise Architect
La diferencia es que, en vez de diagramas UML, llegarán archivos markdown
Esto parece útil para el lado de herramientas de infraestructura
Por ejemplo, se puede imaginar un bundler escrito en Ruby pero compilado de forma estática, que además cumpla el papel de herramienta de instalación de Ruby como RVM
El buildpack actual de Ruby está escrito en Ruby, pero obliga a hacer bootstrap con bash, lo cual es molesto y genera casos borde
CNB se escribió en Rust para evitar ese problema, y la idea de poder distribuir un binario único sin dependencias es realmente poderosa