
# VM de bytecode

La ruta de ejecución predeterminada es un recorrido del árbol: visitar cada nodo AST y devolver un valor. En las funciones muy utilizadas, que se llaman millones de veces, el costo de despacho por nodo es el cuello de botella. Phasis añade un compilador especulativo de bytecode a cierres de PHP para reducir ese costo.

## Cuándo entra en acción la VM

Cada `JsFunction` tiene un contador de llamadas frecuentes. Cuando supera un umbral (100 de forma predeterminada), el motor intenta compilar el cuerpo de la función en un cierre de PHP mediante `src/Bytecode/JsToPhp.php`.

La compilación tiene éxito cuando el cuerpo de la función se ajusta a un conjunto conocido de formas:

- Asignaciones de variables locales con tipos predecibles (numérico, objeto, función, matriz, cadena).
- Operaciones aritméticas con números.
- Lecturas de miembros en variables locales de tipo objeto Y en `this` (`this.v`, `obj.x`).
- Llamadas a métodos en variables locales de tipo objeto (`obj.method(args)`), con el receptor transmitido como `$thisValue`.
- Llamadas al constructor `new Ctor(args)`: promueve el tipo de la variable local del lado izquierdo a `object` y protege con Bailout los retornos que no sean objetos.
- Bucles `for` / `while` con formas de condición simples.
- `return` con una expresión computable.
- `if` / `else` con condiciones tipadas.

La compilación falla (y la función permanece en el recorrido del árbol) cuando el cuerpo contiene:

- Instrucciones `with`.
- `try / catch / finally` (lo gestiona el recorrido del árbol para garantizar la semántica correcta de finalización).
- Llamadas `eval()`.
- Mutación directa de `arguments`.
- Cualquier elemento que el compilador aún no sepa convertir a una forma más baja.

Cuando la compilación tiene éxito, el motor cambia la ruta de llamada de la función al cierre compilado. Las llamadas posteriores ejecutan PHP nativo con el costo de una llamada a función, en lugar de uno por cada nodo AST.

## Abandonos

La ruta rápida compilada parte de ciertas suposiciones (los números siguen siendo números y las propiedades conservan la misma forma). Si una suposición se incumple durante la ejecución, el cierre lanza un `Bailout` y la ejecución se reinicia en el recorrido del árbol para esa llamada.

Aquí es donde más importa la corrección de la especificación. El recorrido del árbol debe poder volver a ejecutar desde cero la llamada que provocó el abandono sin observar efectos secundarios de la ruta rápida fallida. Phasis lo gestiona de dos maneras:

1. **Prefijos puros** — el compilador busca efectos secundarios antes de cualquier especulación. Si la función realiza operaciones de E/S (console.log, funciones del host) antes del punto de especulación, la ruta rápida no las omite: las confirma y continúa.
2. **Rechazo en tiempo de compilación** — para formas que exigirían revertir efectos secundarios observables (una asignación `let r = fn()` cuando `fn()` modifica el estado), el compilador se niega a compilar. La función permanece en el recorrido del árbol.

El caso que motivó esto: una asignación no numérica ejecuta `fn()` en la ruta rápida, lanza `Bailout` y luego se vuelve a ejecutar en el recorrido del árbol, con lo que `fn()` se ejecuta dos veces y produce dos conjuntos de efectos secundarios. El rechazo en tiempo de compilación impide que `fn()` se ejecute en la ruta rápida, así que el recorrido del árbol observa una sola llamada.

## Despacho de la VM (la segunda capa)

Algunas funciones se compilan en cierres de PHP; otras recurren a un intérprete de bytecode aún más sencillo en `src/Bytecode/VM.php`. El bytecode es una matriz plana de opcodes (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …) que consume un despachador `switch`.

La VM existe para las funciones que no vale la pena compilar en un cierre de PHP, pero que se usan lo bastante como para beneficiarse de omitir el recorrido del AST. Las llamadas a métodos en objetos integrados (Array.prototype.map, String.prototype.split, …) siguen rutas rápidas de la VM que evitan el trampolín genérico de `callFunction`.

## Pila de llamadas personalizada

`Op::CALL`, `Op::CALL_METHOD` y `Op::NEW_CALL` usan una ruta de llamada en línea que mantiene toda la pila de llamadas de JS dentro de `VM::execute`, sin crear un nuevo marco de PHP por cada llamada a JS. Al entrar, el estado del llamador (pc, flujo de código, pila de operandos, variables locales, entorno, `this` y marca de modo estricto) se guarda en un grupo de instancias `SavedFrame` de `savedFrames` por VM, y el bucle de despacho redirige sus variables locales a la función llamada. `Op::RET` extrae el marco guardado y restaura el estado. Las excepciones se propagan por los marcos guardados antes de volver al recorrido del árbol.

La elegibilidad se memoriza en `JsFunction`: `vmInlineCallCache` para llamadas normales (cuerpo compilado en BC, no enlazado a `bind()`, no nativo); `vmInlineConstructCache` para expresiones de `new` (además, `isConstructable`, no derivadas, sin inicializadores de campos de instancia ni de métodos privados). Todo lo demás sigue pasando por el trampolín canónico `callFunction` / `vmNewExpression`.

La ventaja arquitectónica adicional: el límite de la pila de PHP ya no restringe la profundidad de recursión de JS. Ahora `CallStack::maxDepth` es el único límite: está establecido en **4 096**, muy por encima de lo necesario para cualquier caso legítimo de test262 (la mayoría requiere menos de 2 k marcos) y es suficientemente estricto para que las pruebas patológicas de recursión infinita fallen rápido, dentro del tiempo límite del ejecutor. La ruta de cierres transpilados (`JsToPhp`) tiene su propia protección de 8 marcos de 192 para la alternativa que no está integrada.

## Generadores con instantáneas de marcos

Los cuerpos de generador sencillos —sin `yield*`, sin `yield` dentro de try/catch/finally, sin `await`— se compilan a bytecode. El compilador emite un nuevo opcode `Op::YIELD` donde se suspende el cuerpo; `Op::YIELD` captura el estado del despacho (cf, pc+1, pila, sp, variables locales, entorno, thisValue, strict) en un objeto de montón `GeneratorSnapshot` y devuelve un valor centinela `YieldResult` desde `VM::execute`. El `JsGenerator` que lo ejecuta almacena la instantánea y se la devuelve a `VM::execute` en la siguiente llamada a `next()` / `throw()`, sustituyendo el valor de reanudación en la parte superior de la pila de operandos.

Esto sustituye la ruta de Fiber de PHP para el subconjunto compatible. El controlador basado en Fiber (`executeGeneratorBody`) sigue en uso para las formas que el compilador rechaza: delegación de `yield*`, generadores que producen valores dentro de un bloque try, generadores asíncronos y funciones asíncronas con `await`. `JsGenerator` funciona en ambos modos: cada `next()` / `throw()` / `return()` comprueba si la instancia tiene una instantánea o un Fiber y actúa en consecuencia.

No hay ningún parámetro ajustable con pruebas de rendimiento; la elegibilidad se decide en tiempo de compilación y se memoriza en `JsFunction`. La ventaja es un cambio de contexto PHP menos por cada valor producido en la ruta de instantáneas; la ruta de Fiber no cambia para los demás casos.

## Cachés en línea

`Op::LOAD_MEMBER` tiene una caché en línea por PC, basada en la identidad del prototipo y usada para el patrón de método del prototipo en una instancia (`c.sum(...)`, `arr.push(...)` y todos los despachos de métodos de clase). Se considera un acierto si el receptor no tiene un espacio propio para el nombre, la referencia al prototipo en caché es idéntica a la de `getPrototype()` del receptor y `PropertyMap::$version` del prototipo aún coincide con la versión capturada.

La invalidación de caché se basa en `PropertyMap::$version`. Las mutaciones estructurales (asignación / eliminación) lo incrementan siempre. Las sobrescrituras de valores de espacios de datos lo incrementan solo cuando `PropertyMap::$isUsedAsProto` es verdadero: una marca de barrera de escritura que se activa la primera vez que se observa el objeto como `[[Prototype]]` de otro objeto. Los objetos de instancia que nunca sirven como prototipos no pagan el costo de la infraestructura de la caché en línea en cada `obj.x = v`.

Los accesores getter nunca se almacenan en caché (deben volver a ejecutarse en cada lectura).

`PropertyMap` tiene un segundo contador, `$mutationVersion`, que se incrementa *siempre* en cada escritura (sin barrera de escritura). La caché `JSON.stringify` fija este contador en cada objeto o matriz anidado que visita, para que una mutación exclusiva de la instancia, como `o.a = 99`, invalide correctamente la cadena almacenada en caché, incluso cuando `$version` de la caché en línea habría omitido el incremento.

## Caché de JSON.stringify

`JsObject` almacena `$jsonCacheString` + `$jsonCacheVersions` (una lista de pares `[obj, mutationVersion]` para cada objeto anidado visitado durante el recorrido de conversión trivial a cadena). En la siguiente llamada a `JSON.stringify(o)` sin función de reemplazo ni espacio, la ruta rápida trivial comprueba que todas las versiones registradas siguen iguales y devuelve la cadena almacenada sin volver a recorrer ni codificar. Los patrones frecuentes de clonación profunda, como `JSON.parse(JSON.stringify(o))`, aceleran aproximadamente 2× la parte de conversión a cadena.

La caché se descarta silenciosamente si no hay coincidencia (cualquier versión registrada es distinta) y se vuelve a llenar en el siguiente recorrido. Los objetos con `toJSON`, getters, accesores, símbolos o espacios `[[PrimitiveValue]]` pasan a la serialización conforme a la especificación y nunca llenan la caché; por eso, la ruta en caché solo procesa datos estructuralmente uniformes.

## Rutas rápidas para funciones integradas

Las mayores mejoras de rendimiento se lograron enseñando a la VM a reconocer funciones integradas específicas:

- **`Atomics.load` / `store` / `compareExchange`** con planificación cooperativa de `loadSpinHook` / `storeNotifyHook`: recuperaron 4 pruebas de bucle de espera de Atomics.
- **`Date.prototype.getTimezoneOffset`** con caché DST de mapa hash directo: mejora de velocidad del 45 % en pruebas de estrés DST de SpiderMonkey.
- **`String.fromCharCode`** con una ruta rápida ASCII integrada: ahorra ~1 M de despachos por ejecución típica.
- **`decodeURI` / `decodeURIComponent`** con una ruta ultrarrápida de 12 caracteres `%XX%XX%XX%XX` que usa decodificación masiva de `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** con acceso directo al mapa de propiedades.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** con iteración nativa sobre la matriz PHP subyacente.

Cada ruta rápida está protegida por una «comprobación de forma»: si el receptor no coincide con la forma esperada (e.g, una vista TypedArray que no está respaldada por un PHP `string` simple), la VM recurre a la ruta lenta que cumple la especificación.

## Lo que esto no hace

La VM de bytecode **no es un JIT** en el sentido de V8. No genera código máquina nativo por función ni especula sobre la base de tipos observados, más allá de la comprobación monomórfica de identidad del prototipo de la caché en línea. La integración en los límites entre funciones se aproxima mediante la pila de llamadas personalizada (sin marco PHP adicional por llamada a JS), en lugar de convertir el cuerpo de la función llamada a bytecode de la función llamadora.

El transpilador JsToPhp es lo más parecido a un JIT que tiene el motor: genera código fuente PHP para cuerpos de funciones muy utilizados que el JIT de trazado de la versión 8.5 de PHP compila después a código nativo. JsToPhp pasa por `eval()`, así que paga un costo de compilación inicial y el JIT de trazado tiene que detectar la función como si fuera código de usuario. Omitir el paso `eval()` emitiendo directamente opcodes de opcache reduciría aún más la diferencia; consulta [Hoja de ruta de pruebas de rendimiento](https://phasis.inline0.dev/es/docs/advanced/benchmarks#roadmap).

## Pruebas de rendimiento

`bench/microbench.js` ejecuta más de una docena de pruebas pequeñas (loop-arith, loop-fib, fn-recurse, fn-deep-recurse, obj-create, obj-prop, proto-method, gen-iterate, arr-push, arr-map, str-concat, str-split-join, json-roundtrip, closure, destructure). Tiempo de ejecución mediano:

```bash
php bench/run.php
```

Los resultados actuales se guardan en `BENCH.md` después de cada ejecución del flujo de trabajo `bench`. Los flujos de trabajo de `bench` y de la matriz test262 se inician manualmente.
