
# VM de bytecode

O caminho de execução padrão é um interpretador de árvore: visita cada nó da AST e retorna um valor. Para funções muito usadas, chamadas milhões de vezes, o custo de distribuição por nó é o gargalo. Phasis adiciona um compilador especulativo de bytecode para closures PHP que reduz esse custo.

## Quando a VM entra em ação

Cada `JsFunction` tem um contador de chamadas frequentes. Quando ele ultrapassa um limite (padrão: 100), o mecanismo tenta compilar o corpo da função para uma closure PHP por meio de `src/Bytecode/JsToPhp.php`.

A compilação é bem-sucedida quando o corpo da função corresponde a um conjunto conhecido de estruturas:

- Atribuições a variáveis locais com tipos previsíveis (numérico, objeto, função, array, string).
- Operações aritméticas com números.
- Leituras de membros em variáveis locais tipadas como objetos E em `this` (`this.v`, `obj.x`).
- Chamadas de métodos em variáveis locais tipadas como objetos (`obj.method(args)`), com o receptor passado como `$thisValue`.
- Chamadas ao construtor `new Ctor(args)` — promove o tipo da variável local do lado esquerdo da atribuição para `object` e protege com Bailout o retorno de um valor que não seja um objeto.
- Loops `for` / `while` com estruturas condicionais simples.
- `return` com uma expressão computável.
- `if` / `else` em condições tipadas.

A compilação falha (e a função permanece no interpretador de árvore) quando o corpo contém:

- Instruções `with`.
- `try / catch / finally` (tratado pelo interpretador de árvore para preservar a semântica correta de conclusão).
- Chamadas `eval()`.
- Mutação direta de `arguments`.
- Qualquer estrutura que o compilador ainda não saiba compilar.

Quando a compilação é bem-sucedida, o mecanismo redireciona o caminho de chamada da função para a closure compilada. As chamadas seguintes executam PHP nativo com o custo de uma chamada de função, em vez do custo de uma chamada por nó da AST.

## Desistências da compilação

O caminho rápido compilado parte de certas premissas (os números continuam sendo números, as propriedades mantêm a mesma estrutura). Quando uma premissa é violada em tempo de execução, a closure lança um `Bailout` e a execução recomeça no interpretador de árvore para aquela chamada.

É aqui que a conformidade com a especificação é mais importante. O interpretador de árvore precisa conseguir reexecutar do início a chamada que levou à desistência da compilação, sem observar efeitos colaterais do caminho rápido que falhou. Phasis trata disso de duas maneiras:

1. **Prefixos puros** — o compilador procura efeitos colaterais antes de qualquer especulação. Se a função realiza E/S (console.log, funções do host) antes do ponto de especulação, o caminho rápido não ignora essa operação; ele a confirma e prossegue.
2. **Recusa em tempo de compilação** — para estruturas que exigiriam reverter efeitos colaterais observáveis (uma atribuição `let r = fn()` não numérica em que `fn()` altera o estado), o compilador se recusa a compilar. A função permanece no interpretador de árvore.

O caso que motivou isso: uma atribuição não numérica executa `fn()` no caminho rápido, lança `Bailout` e depois é executada novamente no interpretador de árvore — executando `fn()` duas vezes, com dois conjuntos de efeitos colaterais. A recusa em tempo de compilação impede que `fn()` seja executado no caminho rápido, então o interpretador de árvore observa uma única chamada.

## Distribuição de chamadas na VM (a segunda camada)

Algumas funções são compiladas para closures PHP; outras recorrem a um interpretador de bytecode ainda mais simples em `src/Bytecode/VM.php`. O bytecode é um array linear de opcodes (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …) consumido por um mecanismo de distribuição `switch`.

A VM existe para funções que não justificam a compilação para uma closure PHP, mas são chamadas com frequência suficiente para se beneficiarem de não percorrer a AST. Chamadas a métodos de objetos integrados (Array.prototype.map, String.prototype.split, …) usam caminhos rápidos na VM que ignoram o trampoline `callFunction` genérico.

## Pilha de chamadas personalizada

`Op::CALL`, `Op::CALL_METHOD` e `Op::NEW_CALL` usam um caminho de chamada inline que mantém toda a pilha de chamadas JS dentro de `VM::execute` — sem criar um novo quadro PHP a cada chamada JS. Ao entrar, o estado do chamador (pc, fluxo de código, pilha de operandos, variáveis locais, ambiente, `this`, sinalizador de modo estrito) é empilhado em um pool `savedFrames` por VM de instâncias `SavedFrame`, e o loop de distribuição redireciona suas variáveis locais para a função chamada. `Op::RET` desempilha o quadro salvo e restaura o estado. As exceções são propagadas pelos quadros salvos antes de retornar ao interpretador de árvore.

A elegibilidade é armazenada em cache em `JsFunction`: `vmInlineCallCache` para chamadas regulares (corpo compilado para BC, não vinculado a `bind()`, não nativo), `vmInlineConstructCache` para expressões `new` (além disso, `isConstructable`, não derivada, sem inicializadores de campos de instância ou métodos privados). Todo o restante continua passando pelo trampoline canônico `callFunction` / `vmNewExpression`.

A vantagem arquitetônica secundária: o limite da pilha PHP não restringe mais a profundidade da recursão JS. Agora, só há o limite `CallStack::maxDepth` — definido como **4 096**, bem acima de qualquer caso legítimo de test262 (a maioria precisa de menos de 2 mil quadros) e suficientemente baixo para que testes patológicos de recursão infinita falhem rapidamente dentro do tempo limite do executor. O caminho de closures transpiladas (`JsToPhp`) tem sua própria proteção de 8 quadros 192 para o fallback sem chamadas inline.

## Geradores com instantâneo do quadro

Corpos simples de geradores — sem `yield*`, sem `yield` dentro de try/catch/finally, sem `await` — são compilados para bytecode. O compilador gera um novo opcode `Op::YIELD` onde o corpo é suspenso; `Op::YIELD` captura o estado de distribuição (cf, pc+1, pilha, sp, variáveis locais, ambiente, thisValue, strict) em um objeto `GeneratorSnapshot` na heap e retorna um valor sentinela `YieldResult` de `VM::execute`. O `JsGenerator` que controla a execução armazena o instantâneo e o devolve a `VM::execute` no próximo `next()` / `throw()`, substituindo o valor de retomada no topo da pilha de operandos.

Isso substitui o caminho da Fiber PHP para o subconjunto compatível. O executor baseado em Fiber (`executeGeneratorBody`) continua disponível para estruturas que o compilador rejeita — delegação de `yield*`, geradores que cedem valores dentro de um bloco try, geradores assíncronos e funções assíncronas com `await`. `JsGenerator` opera nos dois modos: cada `next()` / `throw()` / `return()` verifica se a instância tem um instantâneo ou uma Fiber e executa o caminho correspondente.

Não há parâmetro ajustável para benchmarks; a elegibilidade é decidida em tempo de compilação e armazenada em cache em `JsFunction`. No caminho com instantâneo, a vantagem é eliminar uma troca de contexto PHP por yield; o caminho com Fiber permanece igual em todos os outros casos.

## Caches inline

`Op::LOAD_MEMBER` tem um cache inline por PC indexado pela identidade do protótipo, usado no padrão de método de protótipo em instância (`c.sum(...)`, `arr.push(...)`, toda chamada de método de classe). O cache é válido quando o receptor não tem uma propriedade própria com esse nome, a referência ao protótipo em cache é idêntica à de `getPrototype()` do receptor e `PropertyMap::$version` do protótipo ainda corresponde à versão capturada.

A invalidação do cache depende de `PropertyMap::$version`. Mutações estruturais (definição ou exclusão) sempre o incrementam. Sobrescritas de valores de propriedades de dados só o incrementam quando `PropertyMap::$isUsedAsProto` é verdadeiro — um sinalizador de barreira de escrita ativado na primeira vez em que o objeto é observado como `[[Prototype]]` de outro objeto. Objetos de instância que nunca servem como protótipos não pagam nenhum custo da infraestrutura de IC em cada `obj.x = v`.

Acessores getter nunca são armazenados em cache (precisam ser executados novamente a cada leitura).

`PropertyMap` tem um segundo contador, `$mutationVersion`, que é incrementado *incondicionalmente* a cada gravação (sem barreira de escrita). O cache `JSON.stringify` fixa esse contador em cada objeto/array aninhado visitado, para que uma mutação exclusiva da instância, como `o.a = 99`, invalide corretamente a string em cache mesmo quando o `$version` do IC teria ignorado o incremento.

## Cache de JSON.stringify

`JsObject` mantém `$jsonCacheString` + `$jsonCacheVersions` (uma lista de pares de `[obj, mutationVersion]` para cada objeto aninhado visitado durante a operação trivial de stringify). Na próxima chamada de `JSON.stringify(o)` sem replacer / sem espaço, o caminho rápido trivial verifica se todas as versões fixadas permanecem inalteradas e retorna a string em cache sem percorrer novamente nem recodificar. Padrões frequentes de clonagem profunda, como `JSON.parse(JSON.stringify(o))`, obtêm uma aceleração de cerca de 2× na etapa de stringify.

O cache é descartado silenciosamente em caso de falha (qualquer versão fixada é diferente) e preenchido novamente na próxima passagem. Objetos com `toJSON`, getters, acessores, símbolos ou slots `[[PrimitiveValue]]` recorrem ao serializador conforme a especificação e nunca preenchem o cache — assim, o caminho em cache só recebe dados estruturalmente uniformes.

## Caminhos rápidos integrados

Os maiores ganhos de desempenho vieram de ensinar à VM como lidar com recursos integrados específicos:

- **`Atomics.load` / `store` / `compareExchange`** com escalonamento cooperativo de `loadSpinHook` / `storeNotifyHook` — recuperou 4 testes de loop de espera ativa com Atomics.
- **`Date.prototype.getTimezoneOffset`** com cache DST de tabela hash direta — aceleração de 45 % no teste de estresse DST de SpiderMonkey.
- **`String.fromCharCode`** com caminho rápido ASCII em linha — economiza ~1 M despachos por varredura típica.
- **`decodeURI` / `decodeURIComponent`** com caminho ultrarrápido de 12 caracteres `%XX%XX%XX%XX` que usa decodificação em bloco `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** com acesso direto ao mapa de propriedades.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** com iteração nativa sobre o array PHP subjacente.

Cada caminho rápido é condicionado por uma "verificação de forma" — se o receptor não corresponder à forma esperada (e.g, uma visualização TypedArray sem suporte em um PHP `string` simples), a VM recorre ao caminho lento correto conforme a especificação.

## O que isso não faz

A VM de bytecode **não é um JIT** no sentido de V8. Ela não gera código de máquina nativo por função nem especula com base nos tipos observados além da verificação monomórfica de identidade de protótipo do IC. A incorporação nos limites das funções é aproximada pela pilha de chamadas personalizada (sem quadro PHP extra por chamada de JS), em vez de reduzir o corpo da função chamada ao bytecode da função chamadora.

O transpilador JsToPhp é o mais próximo que o mecanismo chega de um JIT: ele gera código-fonte PHP para corpos de funções frequentemente executados, que o JIT de rastreamento do 8.5 de PHP compila em código nativo. JsToPhp passa por `eval()`, então paga um custo de compilação único e o JIT de rastreamento precisa descobrir a função como se fosse código do usuário. Pular a etapa `eval()` emitindo opcodes do opcache diretamente reduziria ainda mais a diferença — consulte [Roteiro de benchmarks](https://phasis.inline0.dev/pt/docs/advanced/benchmarks#roadmap).

## Benchmark

`bench/microbench.js` executa pouco mais de uma dúzia de benchmarks pequenos (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). Tempo mediano de execução:

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

Os números atuais são registrados em `BENCH.md` após cada execução do fluxo de trabalho `bench`. Os fluxos de trabalho de matriz `bench` e test262 são iniciados manualmente.
