Novedades de Ruby 3.3: Prism y la era de los parsers
Seguimos la serie (todas las entregas, en la etiqueta ruby) con Ruby 3.3, publicado el 25 de diciembre de 2023. Sobre el papel parecía una versión gris: ni una feature de sintaxis vistosa, ni una clase nueva que enseñar en un tuit. Y sin embargo, con la perspectiva de 2026, fue una de las versiones más importantes de la década: la de la fontanería. Parsers, JITs y threads. Lo que no se ve, pero lo sostiene todo.
Prism: por qué reescribir un parser importa
La estrella del anuncio fue Prism, el nuevo parser de Ruby, obra de Kevin Newton. Durante casi treinta años, la gramática de Ruby vivió en parse.y, un fichero generado con Bison que era terreno vedado: quienes se habían asomado ahí dentro volvían canosos. Prism es lo contrario: un parser de descenso recursivo escrito a mano, portable, mantenible y tolerante a errores.
Cada adjetivo importa:
- Portable: pensado para que lo compartan todas las implementaciones —CRuby, TruffleRuby, JRuby— y todas las herramientas que necesitan parsear Ruby: RuboCop, Sorbet, los language servers de tu editor. Hasta 3.3, cada uno mantenía su propio parser con sus propias discrepancias.
- Tolerante a errores: ante código a medio escribir devuelve un AST parcial en vez de rendirse. Justo lo que un LSP necesita para autocompletarte mientras aún te falta un
end.
En 3.3 llegó como default gem (con su API Prism.parse(source)) y como parser alternativo del propio intérprete vía ruby --parser=prism, entonces solo para experimentar:
require "prism"
result = Prism.parse("1 + calamar")
result.success? # => true
result.value # => el AST del programa
De regalo, otra jubilación histórica: Bison fue sustituido por Lrama, un generador de parsers LALR propio. Retrospectiva: la jugada salió redonda. Ruby 3.4 hizo de Prism el parser por defecto, y hoy medio ecosistema de herramientas se apoya en él. Reescribir un parser no da titulares, pero decide qué herramientas puedes construir los diez años siguientes.
YJIT: la versión en la que lo enciendes y te olvidas
Si 3.2 fue el año en que YJIT llegó a producción, 3.3 fue el año en que dejó de haber motivos para no activarlo. Las notas oficiales presumían de más del triple de velocidad que el intérprete en Optcarrot, y de una lista de mejoras muy poco glamurosas y muy efectivas: menos memoria de metadatos, compilación de splats, argumentos opcionales y manejadores de excepciones, y —mi favorita— los call sites megamórficos ya no escapan al intérprete.
Pero el cambio que más consecuencias tuvo fue una simple API:
RubyVM::YJIT.enable
Activar YJIT en caliente, desde el propio código, después del boot. Eso permitió a Rails 7.2 encenderlo por defecto para todo el mundo con Ruby 3.3+ (con config.yjit = false como puerta de salida). Piénsalo: la optimización pasó de "flag que conocen cuatro" a valor por defecto del framework. En mis aplicaciones fue la primera versión en la que el JIT no fue una decisión, sino un hecho. Detalle fino adicional: el umbral de compilación pasa solo de 30 a 120 llamadas cuando la aplicación supera los 40.000 ISEQs, es decir, YJIT se autorregula en apps grandes… como cualquier monolito Rails con solera.
RJIT: un JIT escrito en Ruby (y por qué no debías usarlo)
Ruby 3.3 también estrenó RJIT, un JIT escrito en Ruby puro que sustituyó a MJIT (aquel que compilaba C en un proceso aparte). Solo x86-64 en Unix, sin necesidad de compilador de C en runtime y, según las propias notas, "solo con fines experimentales": para producción, YJIT.
¿Un JIT en Ruby generando código máquina para ejecutar Ruby? Como artefacto intelectual es precioso, y como banco de pruebas para investigar ideas de JIT bajaba muchísimo la barrera de entrada. Retrospectiva honesta: se quedó en anécdota, exactamente como prometía. No conozco un solo despliegue serio con RJIT, y en las notas de 3.4 ya ni aparece. Pero cumplió su papel: mantener viva la experimentación mientras YJIT se llevaba la gloria.
Threads M:N: fontanería para el futuro
La tercera pata invisible: un scheduler M:N de threads, que multiplexa M threads de Ruby sobre N threads nativos del sistema para abaratar su creación y gestión. En el Ractor principal viene desactivado (rompería compatibilidad con extensiones C) y se enciende con RUBY_MN_THREADS=1; el número de threads nativos se controla con RUBY_MAX_CPU (por defecto, 8).
¿Lo notó alguien en una app Rails con Puma? Nadie que yo conozca. Es infraestructura pensada para el mundo que viene: Ractors baratos, miles de threads ligeros, concurrencia de verdad. En 2026 sigue siendo más cimiento que edificio, y lo cuento igualmente porque estas versiones "aburridas" son las que hacen posible lo que luego sí sale en los titulares.
IRB por fin da gusto
Mientras tanto, en la superficie, IRB dio el estirón definitivo: integración avanzada con el debugger vía irb:rdbg, paginador para ls, show_source y show_cmds, autocompletado experimental basado en análisis de tipos y colores configurables con Reline::Face.
Traducción para rails-eros: la rails console de serie alcanzó (y para mí superó) lo que durante una década nos hizo instalar pry en cada Gemfile. Mi ritual de gem "pry-rails" en todo proyecto nuevo murió, en la práctica, con Ruby 3.3.
Detalles que suman
Dos apuntes más que me gusta rescatar. El GC aprendió a promocionar objetos jóvenes con más criterio, reduciendo significativamente la frecuencia de GC mayores —en una app Rails eso es latencia p99, aunque nadie lo celebre—, y defined?(@ivar) se optimizó con object shapes. Y una miga con historia: usar it a secas dentro de un bloque empezó a emitir warning, preparando el terreno para el it como parámetro implícito que llegaría en 3.4. Las deprecaciones bien hechas también son ingeniería.
Retrospectiva desde 2026
Ruby 3.3 es la versión que peor se vendía en una charla relámpago y la que mejor ha envejecido en la sala de máquinas. Lo que importó: Prism (hoy parser por defecto y columna vertebral del tooling), el YJIT que Rails enciende solo, un IRB de primera. Lo que se quedó en anécdota: RJIT, con honores, y un scheduler M:N que sigue esperando su momento.
Si la serie demuestra algo es que las versiones de Ruby que cambian tu día a día no siempre son las que estrenan sintaxis. Las notas oficiales, aquí; el desglose minucioso, en rubychanges.