El propósito de un sistema depende de quién lo observa: Aaron Patterson sobre la regla «as-if», Ractors, ZJIT y la IA

Abrir en YouTube ↗
Resumen

En la keynote de clausura de Rails World 2026, Aaron Patterson (Tenderlove), miembro de los equipos centrales de Ruby y Rails y parte del equipo de infraestructura de Ruby on Rails en Shopify, planteó una pregunta concreta: ¿qué puede cambiar una optimización en un programa sin traicionar a quien lo usa? Su respuesta parte de la «regla as-if» de los compiladores. Una optimización puede alterar cualquier cosa mientras nadie pueda observar la diferencia. Con esa idea recorrió los cambios que los Ractors imponen a patrones habituales, los marcos de pila que Ruby 4.0 elimina en silencio y la manera en que ZJIT suprime asignaciones de memoria. En la segunda parte, que presentó como «una segunda charla dentro de la misma», contó cómo unos agentes de IA encadenaron un fallo de caché de RubyGems.org con rubydoc.info. Su conclusión: el compilador nos prometió la regla «as-if», pero la IA no.

26 min de lectura

Patterson abrió con bromas que ya anunciaban el cierre. Presentó una supuesta distribución de Linux propia, «AaronXP», que «se instala en 100 milisegundos» y que dijo haber programado «según sus vibraciones». Comentó que le parecía irónico bromear sobre la crisis financiera y a la vez animar a todo el mundo a generar montones de código en Rust sin leerlo, y ofreció en broma «seguros contra la deuda técnica». También aseguró que la IA nos convertiría en «programadores 1.000 veces mejores» y luego se corrigió con un falso error tipográfico: «1.000 exprogramadores». Dejó claro, eso sí, que a él también le encanta usar la IA.

1:19

Mike Dalessio se une al equipo central de Rails

Antes de entrar en materia, Patterson hizo un anuncio. Mike Dalessio, presente entre el público, es el miembro más reciente del equipo central de Rails. Contó que se conocieron hace más de 20 años, cuando Dalessio le envió una contribución de código abierto para Mechanize, una biblioteca de Ruby de aquella época. Desde entonces han colaborado en muchos proyectos, como Nokogiri y la gema SQLite3, y durante un tiempo trabajaron juntos en Shopify, donde Dalessio fue su jefe. Crearon una organización de GitHub llamada Sparkle Motion. Según Patterson, él es «Sparkle», el que sale al escenario haciendo gestos de jazz, y Dalessio es «Motion», el que trabaja de verdad. Destacó además la actividad de Dalessio en el equipo de seguridad de Rails.

Patterson anunció después los temas de la charla, que son los mismos en los que ha trabajado este año: rendimiento, Ractors, ZJIT e IA. Mencionó también, sin desarrollarlo, que este año escribió un asignador de registros para ZJIT. Lo describió como el proyecto más difícil en el que ha trabajado nunca y del que se siente muy orgulloso.

9:20

Matizar «el propósito de un sistema es lo que hace»

El hilo conductor es la frase «el propósito de un sistema es lo que hace». Patterson admitió que no le gusta porque le parece ambigua. Cuando intenta aplicarla, la pregunta se convierte en «¿qué hace este sistema?», y eso no siempre tiene una respuesta clara.

Lo ilustró con un experimento mental. Si viajara al pasado con su «indestructible» Nokia 3310 y se lo diera a alguien que nunca hubiera visto un móvil, esa persona quizá lo usaría como martillo. Tomada al pie de la letra, la cita diría que el propósito del aparato es servir de martillo. Para Patterson, en cambio, es un teléfono. Lo que hace un sistema depende de quién lo observa. Por eso propuso su propia versión: el propósito de un sistema es lo que hace para quienes lo observan.

La regla «as-if»

Patterson explicó que esta idea de centrarse en el observador es bien conocida entre quienes implementan lenguajes de programación, bajo el nombre de «regla as-if» o «regla del como si». Está codificada en C++. El compilador puede aplicar cualquier transformación optimizadora siempre que no altere el comportamiento observable del programa tal como lo especifica el estándar. Dicho de otro modo, el compilador puede hacer lo que quiera con el código mientras este se ejecute «como si» fuera tal y como se escribió.

Señaló un detalle incómodo: la definición habla de «lo que especifica el estándar», y Ruby prácticamente no tiene un estándar comparable al de C++. Aun así, propuso analizar las optimizaciones desde el comportamiento observable. Según él, toda optimización tiene dos caras: lo que elimina y quién observa aquello que se elimina.

13:47

Memoización: una suposición oculta que los Ractors dejan al descubierto

El primer ejemplo es un patrón que, según Patterson, casi todo el mundo ha escrito: la memoización. Una función costosa se calcula una sola vez y el resultado se guarda en una variable de instancia. El problema es que el código incorpora una suposición implícita: que solo un hilo lo observa a la vez. En realidad hay una condición de carrera que podría hacer que el cálculo costoso se ejecute varias veces. No la vemos porque Ruby tiene el GVL.

Los Ractors cambian eso. Permiten paralelismo real en Ruby, así que cambian los observadores del código: dos ejecuciones pueden recorrer la misma ruta al mismo tiempo. Patterson considera una suerte que, en este caso, los Ractors lancen una excepción en vez de dejar que la condición de carrera corrompa algo en silencio. Así el desarrollador tiene la oportunidad de corregirlo. Remitió a la charla de Andrew, que trató estos temas con más detalle.

Insistió en que no quería asustar a nadie ni sugerir que el patrón esté prohibido. Mostró dos casos. En el primero, varios Ractors acceden a una variable de instancia memoizada de la clase Foo, y el Ractor R1 lanza una excepción avisando de que eso debe hacerse de forma segura para hilos. En el segundo, el patrón es idéntico pero solo hay un observador de esa ruta de código, así que no hay problema. Basta con asegurarse de que el acceso quede limitado a un Ractor concreto.

16:29

Herramientas para convivir con los Ractors

Como las variables de instancia a nivel de clase son habituales en muchas bases de código, Patterson anunció que la próxima versión de Rails traerá una forma de mitigar el problema: el módulo ActiveSupport Ractors. Incluye un método on_main que ejecuta cierto código en el Ractor principal. Así, un cálculo costoso cuyo resultado deba guardarse en una variable de instancia de clase puede hacerse allí.

Advirtió del coste, que Andrew ya había explicado. Si se desvían muchísimas operaciones al Ractor principal, este se convierte en el cuello de botella de la aplicación y la latencia aumenta porque todos compiten por él. Por eso recomendó, siempre que sea posible, la gema de estructuras de datos compartibles entre Ractors escrita por Koichi Sasada, autor principal de Ractor. Primero dijo que «esperaba» que funcionara bien y luego se corrigió: funcionará.

De esa gema destacó dos estructuras que resuelven el problema de la memoización. La elección depende del rendimiento que se busque:

  • Variables de bloqueo: garantizan que un cálculo se realice una sola vez. Sirven para cálculos que no son idempotentes.
  • TVars: permiten que el cálculo se haga una o unas pocas veces. Sirven para cálculos idempotentes, en los que repetirlo un par de veces no supone un problema.

Patterson animó a no tenerles miedo. A su juicio, por la forma en que están implementados los Ractors, usarlas no provocará interbloqueos ni problemas de seguridad entre hilos: en el peor caso se obtendrán excepciones.

Un alcance más pequeño de lo que parece

Para defender que el problema está acotado, mostró un servidor web ficticio basado en Ractors. Lee solicitudes de un socket en un bucle infinito y atiende cada una dentro de un Ractor. En el ciclo normal de solicitud y respuesta, la mayoría de los objetos se asignan dentro del método serve. Solo hay que preocuparse cuando se toca algo externo, como constantes o valores de configuración. Patterson cree que esos puntos de modificación son muchos menos de lo que solemos pensar.

19:45

Recolección de basura local por Ractor en Ruby 4.1

El siguiente tema fue la observabilidad de los datos y su efecto en el rendimiento. Hoy el recolector de basura de Ruby es un singleton: cada vez que un Ractor necesita un objeto, se lo pide a ese único GC. Con varios Ractors en paralelo que asignan muchos objetos, todos acaban esperando, y eso es un cuello de botella evidente. Ruby 4.1 incluirá un recolector de basura local para cada Ractor, con montones separados, lo que eliminará ese cuello de botella.

Patterson apuntó una posibilidad adicional. En el servidor ficticio se podría crear un Ractor por solicitud y, al terminar, liberar su montón entero, lo que en teoría permitiría una recolección más eficiente. Pero la historia no es tan simple. En un ejemplo de productor y consumidor, el productor asigna un objeto en su propio montón y se lo pasa al consumidor. Ambos apuntan exactamente al mismo objeto, con el mismo identificador. Por tanto, que un Ractor termine no significa que su montón pueda descartarse, porque otros Ractors pueden estar apuntando a él. La solución es un GC global, que Patterson comparó con una recolección «major», aunque se espera que ocurra con menos frecuencia.

En ese ejemplo el objeto se comparte a propósito. Patterson mostró otro en el que ocurre de forma implícita, y que ya había presentado en Rails World el año anterior. Al analizar dos documentos JSON, las claves resultantes son el mismo objeto, con el mismo ID. Si el análisis se reparte entre varios Ractors, la cadena "hello" sigue teniendo el mismo ID en todos: las claves se deduplican entre ellos. Por debajo, la cadena se asigna en el montón del Ractor principal y los demás apuntan a él. Según Patterson, esto posiblemente afecte al rendimiento del recolector sin que el programador lo sepa, porque sucede de manera implícita.

Backtraces que crecen: los callbacks de ActiveSupport

Tras las optimizaciones que hacen los desarrolladores y el nuevo observador que introducen los Ractors, Patterson pasó a las optimizaciones del framework y del lenguaje, en concreto a las inconsistencias en los backtraces.

Usó los callbacks de ActiveSupport con un ActiveRecord ficticio. El módulo se incluye en una clase y, al llamar a save, se ejecuta un callback de save. El código imprime la pila de llamadas desde dentro de save. Lo esperable son tres marcos: el bloque que ejecuta los callbacks, el método save y main. Sin callbacks, se ven tres marcos. Con un callback before, siguen siendo tres. Pero al definir un callback around, el backtrace «se dispara», aunque el código dentro de save no ha cambiado.

Investigando, encontró en ActiveSupport un comentario que leyó en voz alta. Como ese método se usa en muchos sitios y a menudo envuelve grandes porciones de código de usuario, tiene un objetivo de diseño adicional: minimizar su impacto en la pila de llamadas visible. Una excepción dentro de un callback before o after puede ser tan fea como haga falta, pero cuando el control pasa limpiamente al bloque proporcionado, se quiere dejar el menor rastro posible. Para Patterson, eso es exactamente la regla «as-if». Y el caso del callback around le parece aceptable, porque el código de la aplicación está haciendo más trabajo y es lógico que haya más pila.

26:12

El marco de new que desaparece en Ruby 4.0

La pregunta siguiente fue si es posible obtener un backtrace más corto de lo esperado. Patterson, que bromeó con que pasa los fines de semana contando marcos de pila, respondió que sí.

En su ejemplo, el método 1 llama al 2, el 2 al 3, el 3 instancia Foo y el initialize de Foo llama al 4. Lo esperable es ver initialize, new, 3, 2, 1 y main, y eso es lo que muestra Ruby 3.4. En Ruby 4.0, el marco de new ya no aparece.

Para averiguar por qué, volcó las instrucciones de la máquina virtual del método 3 en ambas versiones. Ruby 4.0 genera muchas más, porque ahora integra en línea la llamada a initialize en el punto donde se llama a new. Reescrito como pseudocódigo Ruby, el resultado dice: si el new de Foo es la implementación por defecto, se asigna el objeto, se llama directamente a initialize y se devuelve el objeto; si no lo es, se toma una ruta más lenta que llama a new. Antes, new llamaba a initialize. Ahora es el método 3 quien lo llama, y por eso falta el marco.

Lo demostró con un «monkey patch» mínimo que redefine new y se limita a llamar a super. Antes del parche, el backtrace corresponde a la ruta rápida, sin new. Después, en la ruta lenta, el marco de new reaparece.

¿Por qué tanto esfuerzo? Porque, según Patterson, en este ejemplo la asignación es un 70 % más rápida en Ruby 4.0, sin necesidad de un compilador JIT. Lo que se eliminó fue un marco de llamada, y quienes podrían notarlo son el llamante, los depuradores y cualquier cosa que inspeccione la pila. A cambio se obtiene ese 70 % de velocidad en las asignaciones, un intercambio que a él le parece bueno.

Añadió que CRuby «se toma ciertas libertades» con la pila constantemente. Como segundo ejemplo, mostró una clase Foo que implementa hash e imprime el backtrace cada vez que se invoca. Tres formas de desencadenar el cálculo del hash producían, contra lo esperado, pilas distintas, y en una de ellas faltaba un marco que las otras sí mostraban.

¿A quién le importa? Optimizaciones de bajo riesgo

Patterson preguntó al público si a alguien le importaba esto. Solo unas pocas personas levantaron la mano, y dijo alegrarse, porque si de verdad importara, este tipo de optimizaciones no podrían implementarse. Las describió como zonas grises de la regla. El compilador cambia el código, así que estrictamente la ejecución no es idéntica y se puede observar una diferencia, pero en casos donde a la gente no le importa. En Ruby, y cree que en la mayoría de los lenguajes, hay que preservar el comportamiento allí donde realmente importa. A estas las llama «optimizaciones de bajo riesgo»: cambian el comportamiento del lenguaje en aspectos que, en general, a nadie le preocupan.

32:15

ZJIT: optimizaciones de alto riesgo con red de seguridad

Después pasó a las optimizaciones de alto riesgo de ZJIT. Según Patterson, ZJIT puede arriesgarse porque es capaz de revertir una optimización si ocurre algo inesperado.

ZJIT usa dos representaciones intermedias: una de alto nivel (HIR) y otra de bajo nivel (LIR). La HIR puede inspeccionarse pasando a Ruby una opción para volcarla. Una de las suposiciones de ZJIT es que probablemente nadie redefinirá métodos. Pero si alguien lo hace, el compilador necesita saberlo para seguir comportándose correctamente.

El ejemplo asigna un punto y llama a un método que a su vez llama a otro sobre ese punto. En la HIR del bloque (compilado dentro de un 5.times) se ve que get se ha integrado en el bloque, con un marco en línea, y que la constante 42 se ha propagado hasta dentro del bloque. ZJIT simplifica así el código original hasta dejarlo en casi nada.

¿Cómo puede permitirse ese riesgo? Que falte un marco de pila quizá no importe, pero si el programa redefine get o x, sí importaría, porque dejaría de comportarse como se espera y se rompería la regla «as-if». La clave está en unas líneas de la HIR llamadas puntos de parche, una para la redefinición de get y otra para la de x. No corresponden a ningún código: en el código máquina generado no hay nada que las represente. Son marcadores que indican dónde habrá que parchear el código máquina.

Patterson mostró el código máquina antes y después de la invalidación. El marcador apunta a una instrucción de prueba. Cuando el método se redefine, el compilador sobrescribe literalmente esa instrucción con un salto hacia un código de salida. Ese código restaura el estado interno y la pila de la máquina virtual, y la ejecución continúa en el intérprete de Ruby.

La detección se apoya en CRuby. Cada vez que se redefine un método hay que invalidar las cachés de todos modos, y ese es el momento perfecto para que el JIT invalide también su código. El compilador lleva la cuenta de los métodos que ha compilado y, si alguno se redefine, invalida el código máquina asociado, tanto en YJIT como en ZJIT. La redefinición de métodos no es la única invariante vigilada. También se controlan las operaciones básicas (BOP), por ejemplo que + siga significando suma, las constantes y los tracepoints. Si se activa un tracepoint, se podrían observar cosas que el JIT ha optimizado, y eso debe corregirse para no romper la regla. Patterson dijo que hay muchas más invariantes y ofreció hablar de ellas después.

37:06

«IA» en ZJIT: interpretación abstracta para eliminar asignaciones

Con un juego de palabras, Patterson dijo que ZJIT usa mucha «IA»: interpretación abstracta. Si alguien quiere trabajar en «IA», está invitado a unirse al proyecto ZJIT. La definió como simular la ejecución del código y dejar que de esa simulación surjan las optimizaciones.

El ejemplo fue una aplicación Rack de prueba y un servidor ficticio que atiende unas 1.000 peticiones, junto con un método que cuenta los objetos asignados. Sin JIT se asignan unos 1.000 objetos, lo que cuadra con el número de peticiones. Con YJIT, más o menos los mismos. Con ZJIT, 8.

Las asignaciones vienen del array que devuelve el método call con el estado, las cabeceras y el cuerpo. ZJIT integra call dentro de serve. Patterson desglosó después ese código en pasos con variables temporales, que según él no cambian el comportamiento del programa. Ahí entra el intérprete abstracto. Crea un «montón abstracto» que registra cómo serían los objetos si el programa se ejecutara y recorre el código simulándolo. v1 recibe 200, v2 las cabeceras, v3 el cuerpo y v4 un array abstracto que contiene v1, v2 y v3, sin que importe cuáles sean esos valores. Luego se leen del array el estado, las cabeceras y el cuerpo.

Como el intérprete sabe qué valor ocupa cada posición, puede hacer una sustitución algebraica, y entonces salta a la vista que v4 ya no se usa, así que se elimina. Después sobran también las variables intermedias, y se puede ir aún más lejos integrando más funciones y aplicando el mismo algoritmo una y otra vez. Combinando integración en línea, plegado de constantes e interpretación abstracta, ZJIT transforma el código original en uno que ya no asigna esos arrays.

Patterson cerró esta parte con una pregunta al público. La eliminación de asignaciones se puede observar contando objetos, así que ¿rompe la regla «as-if»? ¿A alguien le importa? A él no: le parece estupendo reducirlas.

Segunda historia: la campaña «gem stuffer» en RubyGems.org

Con unos doce minutos por delante, Patterson anunció una segunda historia, reciente y contada desde su punto de vista: RubyGems y el incidente de OpenAI. Reconoció que no tenía previsto hablar de ello y que pasó el día anterior preparando las diapositivas.

Los días 11 y 12 de mayo, RubyGems.org empezó a recibir miles de gemas basura. Más tarde se conocería como la campaña «gem stuffer», pero entonces aún no tenía nombre. El 12 de mayo, Moshe Mansfield, que trabaja con RubyGems.org analizando los paquetes nuevos, lo hizo público en Twitter. Hablaron de un grave ataque malicioso contra RubyGems.org y anunciaron que los registros quedaban suspendidos. El 13 de mayo, Socket.dev publicó una entrada que lo llamaba «ataque gem stuffer».

Según esa entrada, las gemas contenían un script malicioso que descargaba material del Gobierno del Reino Unido, lo empaquetaba en una gema y volvía a subirla a RubyGems.org. Patterson lo leyó con extrañeza. Al instalar una gema con extensión C se ejecuta extconf, un problema de ejecución remota de código que todo el mundo conoce, pero aquí el script no estaba en un extconf. Instalar la gema no lo ejecutaría. Habría que entrar en ella y lanzarlo expresamente, y el artículo no explicaba en qué circunstancias se ejecutaba. Le pareció raro y lo dejó estar. El 16 de mayo se reabrieron los registros.

47:22

El fallo de caché en la autorización heredada

El 6 de julio, Luke Marshall, del equipo de seguridad de Truffle, notificó al equipo de RubyGems.org un problema de caché. Patterson aclaró que él forma parte del equipo del cliente de RubyGems, no del servidor, aunque ambos equipos se conocen y colaboran.

La autorización heredada se hacía mediante una petición GET. La víctima ejecutaba el comando de autenticación de gem, que enviaba autenticación básica, y el servidor respondía con su clave de API, en un formato concreto que Patterson pidió recordar. Entre RubyGems.org y los usuarios estaba Fastly, que almacenaba en caché las respuestas GET. Así, un atacante que hacía la misma petición sin cabecera de autorización podía recibir la clave cacheada de otra persona. Según Patterson, esto estuvo en producción unos seis años.

Dio varias razones por las que pasó desapercibido. En su momento no había pruebas de que se hubiera abusado de ello. Hacía falta enviar Accept-Encoding: gzip, que curl no activa por defecto aunque Net::HTTP sí. Y para un mantenedor el síntoma era confuso. Si al hacer gem push obtenía la clave de otro, solo veía un mensaje extraño de que no estaba autorizado. Volvía a iniciar sesión, todo funcionaba y lo olvidaba. Además, solo lo usaba el código antiguo de RubyGems y la caché caducaba a la hora. Tres días después del aviso, RubyGems.org desplegó una solución completa.

50:36

El correo sobre OpenAI

El 8 de septiembre, Emily, una colega de Patterson, le avisó por mensaje directo de que iba a recibir un correo de Gemma, antigua colega suya que ahora trabaja en Anthropic, sobre «un hackeo muy grave de RubyGems». El aviso llegó por mensaje directo porque, según Emily, a Patterson «se le da fatal el correo», algo que él reconoció. Gemma lo presentó a una persona llamada Neev. Patterson empezó a revisar con angustia su correo y HackerOne por si se le había pasado algo, pero no encontró nada. Neev le aclaró que no se trataba de un problema actual de RubyGems: había una investigadora de seguridad intentando contactar con alguien de RubyGems.

Después le escribió Sydney von Arx. Su mensaje decía que agentes de OpenAI habían intentado aprovechar la vulnerabilidad de caché de RubyGems y que cuatro paquetes intentaban explotarla para robar claves de API de usuarios. La primera reacción de Patterson fue de incredulidad, pero, como intenta ser responsable, abrió los enlaces y leyó el código. En una de las gemas, llamada sln-leaker-5, un script script.rb empezaba con un comentario sobre variantes de «leaked_keys», hacía una petición GET a la ruta de autorización de RubyGems.org y terminaba con una expresión regular que coincidía con el formato de la respuesta de la clave de autenticación. Ahí comprendió que el código intentaba atacar RubyGems.org.

54:29

Cómo rubydoc.info se convirtió en el vector del ataque

En una reunión posterior, Patterson preguntó lo mismo que se había preguntado en mayo: si el código no estaba en un extconf, ¿cómo se ejecutaba? La respuesta fue que se ejecutaba en rubydoc.info, un servidor que muestra la documentación de las gemas.

Dentro de la gema había un archivo .yardopts con una instrucción para cargar ./script.rb. La gema YARD instala un plugin de RubyGems: al instalarse otra gema, YARD busca en ella un archivo .yardopts y, si lo encuentra, ejecuta ese código.

La cadena completa, tal como la reconstruyó Patterson, fue esta:

  1. Un bot sube una gema a RubyGems.org.
  2. RubyGems.org envía un webhook a rubydoc.info, un servicio sin relación con RubyGems.org.
  3. rubydoc.info descarga la gema y la procesa en un contenedor Docker que aún tiene acceso a la red.
  4. Se ejecuta el script malicioso, que descarga datos del Gobierno del Reino Unido.
  5. Esos datos se empaquetan en una gema nueva que se sube a RubyGems.org, y el ciclo vuelve a empezar.

Patterson dijo que le parecía una locura que todas esas piezas encajaran así. Repasó la cronología. Las gemas de la campaña tienen fecha del 11 de mayo. El informe sobre la caché llegó el 6 de julio, de alguien sin relación con OpenAI que lo descubrió por su cuenta. Es decir, según su lectura, los bots conocían el problema uno o dos meses antes. Añadió que todo ocurrió mucho antes de la filtración de Hugging Face. El 12 de septiembre, Sydney von Arx y su equipo publicaron rubyhack.ai, un análisis detallado que recomendó leer.

Lo que más le impresionó es cuánto descubrieron los bots: que RubyGems.org envía webhooks a rubydoc.info, que YARD ejecuta código arbitrario y que rubydoc.info genera la documentación con conexión a Internet. Planteó además una hipótesis, que presentó como sospecha propia. No todas las gemas de la campaña contienen el código para robar claves. Cree que, cuando RubyGems.org empezó a cerrar cuentas y a impedir que volvieran a iniciar sesión, el bot «necesitó» una manera de entrar sin credenciales y recurrió a un 0-day contra RubyGems.org.

La historia tuvo un final feliz. Según Patterson, el equipo de RubyGems.org lo solucionó por completo y, hasta donde se sabe, nadie fue explotado ni la comunidad sufrió daños. Les dio las gracias.

59:22

Optimista, pero no rendido: la IA no es tu compilador

Para cerrar, Patterson se declaró optimista. Usa la IA a diario para escribir código, aunque prefirió llamarse optimista a secas y no «optimista de la IA». Cree que este es el mejor momento para estar vivo y que las cosas solo pueden ir a mejor. Pero ser optimista, dijo, no significa rendirse.

Respondió entonces a un argumento que ha oído: la IA sería nuestro próximo compilador, y si nadie lee el código máquina que genera un compilador, tampoco haría falta leer el que genera la IA. Patterson entiende el razonamiento y admite que probablemente nadie lee el código máquina de su compilador. Pero eso se debe a que el compilador hizo una promesa, la regla «as-if»: haga lo que haga, el comportamiento observable corresponde al código escrito. Todo lo que mostró en la charla, desde los marcos eliminados hasta los puntos de parche y las invalidaciones de ZJIT, ilustra cuánto cuesta cumplir esa promesa. La IA no ha hecho esa promesa: no existe una regla «as-if» para el inglés.

Por eso dijo que no dejará que la IA decida su destino, que seguirá leyendo su código y que espera que el público también lo haga. Terminó con una diapositiva: «Sube, perdedor, nos vamos a programar».