
# 字节码 VM

默认执行路径是树遍历器——访问每个 AST 节点并返回一个值。对于被调用数百万次的热点函数，每个节点的分发开销会成为瓶颈。Phasis 在此基础上加入了推测式字节码到 PHP 闭包编译器，以降低这部分开销。

## VM 何时启动

每个 `JsFunction` 都有一个热点调用计数器。计数超过阈值（默认为 100）后，引擎会尝试通过 `src/Bytecode/JsToPhp.php` 将函数体编译为 PHP 闭包。

当函数体符合一组已知形态时，编译即可成功：

- 对类型可预测的局部变量进行赋值（数值、对象、函数、数组、字符串）。
- 对数字执行算术运算。
- 读取对象类型的局部变量和 `this` 的成员（`this.v`、`obj.x`）。
- 调用对象类型局部变量的方法（`obj.method(args)`），并将接收者作为 `$thisValue` 传入。
- 调用 `new Ctor(args)` 构造函数——将左侧局部变量的类型提升为 `object`，并通过退出保护检查返回值是否为对象。
- 条件形态简单的 `for` / `while` 循环。
- 表达式可计算的 `return`。
- 基于类型化条件的 `if` / `else`。

当函数体包含以下内容时，编译会失败（函数将继续由树遍历器执行）：

- `with` 语句。
- `try / catch / finally`（由树遍历器处理，以保证完成语义正确）。
- `eval()` 调用。
- 直接修改 `arguments`。
- 编译器尚不支持降级处理的任何内容。

编译成功后，引擎会将该函数的调用路径切换为已编译的闭包。之后的调用只需承担一次函数调用开销，即可执行原生 PHP，无需为每个 AST 节点分别付出开销。

## 退出机制

编译后的快速路径会依赖一些假设（数字始终是数字，属性始终保持相同形态）。运行时若假设失效，闭包会抛出 `Bailout`，该次调用随即在树遍历器中重新执行。

规范正确性在这里至关重要。树遍历器必须能够从头重新执行触发退出的调用，且不能观察到快速路径失败所产生的副作用。Phasis 通过两种方式处理这一点：

1. **纯前缀** — 编译器会在推测开始前检查副作用。如果函数在推测点之前执行了 I/O（console.log、宿主函数），快速路径不会省略该操作，而是先提交再继续。
2. **编译时拒绝** — 对于需要回滚可观察副作用的形态（例如非数值的 `let r = fn()`，而 `fn()` 会修改状态），编译器会直接拒绝编译。该函数会继续由树遍历器执行。

典型情况是：快速路径执行非数值赋值 `fn()`，抛出 `Bailout`，随后在树遍历器中重新执行——导致 `fn()` 运行两次并产生两组副作用。编译时拒绝可避免 `fn()` 在快速路径中运行，因此树遍历器只会观察到唯一一次调用。

## VM 分发（第二层）

部分函数会编译为 PHP 闭包；其他函数则会进入 `src/Bytecode/VM.php` 中更简单的字节码解释器。字节码是由操作码组成的扁平数组（`LOAD_LOCAL`、`STORE_PROP`、`CALL_METHOD`、…），由 `switch` 分发器执行。

对于那些不值得编译为 PHP 闭包、但调用频率足以从跳过 AST 遍历中获益的函数，VM 就能派上用场。对内置对象（Array.prototype.map、String.prototype.split、…）的方法调用会经过 VM 中的快速路径，绕过通用的 `callFunction` 跳板。

## 自定义调用栈

`Op::CALL`、`Op::CALL_METHOD` 和 `Op::NEW_CALL` 使用内联调用路径，将整个 JS 调用栈保存在 `VM::execute` 中——每次 JS 调用都不会新建 PHP 帧。进入时，调用方状态（pc、代码流、操作数栈、局部变量、env、thisValue、`this`、严格模式标志）会被压入每个 VM 专属的 `savedFrames` 池中，该池存放 `SavedFrame` 实例；随后分发循环会将其局部变量切换为被调用函数的局部变量。`Op::RET` 会弹出并恢复已保存的帧。异常会沿保存的帧逐层展开，然后回退到树遍历器。

是否适用会缓存在 `JsFunction` 上：常规调用为 `vmInlineCallCache`（已用 BC 编译的函数体、非 `bind()` 绑定、非原生函数）；`vmInlineConstructCache` 用于 `new` 表达式（还要求满足 `isConstructable`、非派生类，且没有实例字段或私有方法初始化器）。其他情况仍会经过标准的 `callFunction` / `vmNewExpression` 跳板。

在架构上的另一项好处是：PHP 栈上限不再限制 JS 递归深度。现在唯一的限制是 `CallStack::maxDepth`——设为 **4 096**，远高于所有合理的 test262 情况（大多数需要不到 2 k 帧），同时也足够严格，能让病态的无限递归测试在运行器的超时限制内快速失败。转译闭包路径（`JsToPhp`）对非内联回退路径另有 8 个 192 帧的保护限制。

## 帧快照生成器

简单的生成器主体（没有 `yield*`，不会在 try/catch/finally 内使用 `yield`，也没有 `await`）会编译为字节码。主体暂停时，编译器会生成新的 `Op::YIELD` 操作码；`Op::YIELD` 会将分派状态（cf、pc+1、stack、sp、locals、env、thisValue、strict）捕获到 `GeneratorSnapshot` 堆对象中，并从 `VM::execute` 返回 `YieldResult` 哨兵值。驱动 `JsGenerator` 会保存快照，并在下一次 `next()` / `throw()` 时将其传回 `VM::execute`，替换操作数栈顶部的恢复值。

对于受支持的子集，这取代了 PHP-Fiber 路径。基于 Fiber 的驱动程序（`executeGeneratorBody`）仍用于编译器拒绝的形态——`yield*` 委托、在 try 块内 yield 的生成器、异步生成器，以及带有 `await` 的异步函数。`JsGenerator` 支持双模式：每次 `next()` / `throw()` / `return()` 都会检查实例使用的是快照还是 Fiber，并据此分发。

没有可供基准测试调节的参数；是否适用会在编译时决定并缓存在 `JsFunction` 上。快照路径的优势是每次 yield 少一次 PHP 上下文切换；其他情况仍使用原有的 Fiber 路径。

## 内联缓存

`Op::LOAD_MEMBER` 会为每个 PC 维护一个以原型标识为键的内联缓存，用于原型方法在实例上的模式（`c.sum(...)`、`arr.push(...)` 以及所有类方法分发）。命中条件：接收者没有该名称的自有槽位，缓存的原型引用与接收者的 `getPrototype()` 相同，并且原型的 `PropertyMap::$version` 仍与捕获的版本一致。

缓存失效依赖 `PropertyMap::$version`。结构变更（设置 / 删除）总会使其递增。数据槽值被覆盖时，只有在 `PropertyMap::$isUsedAsProto` 为 true 时才会递增——这是一个写屏障标志，在对象首次被观察到作为另一个对象的 `[[Prototype]]` 时翻转。永不作为原型的实例对象无需为每次 `obj.x = v` 支付 IC 基础设施的开销。

Getter 访问器从不缓存（每次读取都必须重新运行）。

`PropertyMap` 还维护第二个计数器 `$mutationVersion`，每次写入都会*无条件*递增（没有写屏障）。`JSON.stringify` 缓存会在每个访问过的嵌套对象 / 数组上固定记录这个计数器，因此即使 IC 的 `$version` 会跳过递增，类似 `o.a = 99` 这样的仅实例变更也能正确使缓存字符串失效。

## JSON.stringify 缓存

`JsObject` 会保存 `$jsonCacheString` + `$jsonCacheVersions`（一组 `[obj, mutationVersion]` 对，用于记录简单 stringify 遍历中访问过的每个嵌套对象）。下一次调用 `JSON.stringify(o)` 时，如果没有 replacer / space，简单快速路径会验证所有固定记录的版本均未变化，然后直接返回缓存字符串，无需重新遍历或编码。像 `JSON.parse(JSON.stringify(o))` 这样的热点深克隆模式，可让 stringify 部分提速约 2 倍。

缓存未命中时会静默丢弃（任一固定记录的版本不同），并在下一次遍历时重新填充。含有 `toJSON`、getter、访问器、符号或 `[[PrimitiveValue]]` 槽位的对象会回退到规范序列化器，且永远不会填充缓存，因此缓存路径只会处理结构统一的数据。

## 内置对象快速路径

最大的性能提升来自让 VM 针对特定内置对象进行优化：

- **`Atomics.load` / `store` / `compareExchange`** 配合 `loadSpinHook` / `storeNotifyHook` 协作式调度——修复了 4 项 Atomics 自旋循环测试。
- **`Date.prototype.getTimezoneOffset`** 使用直接哈希映射 DST 缓存——SpiderMonkey DST 压力测试提速 45 %。
- **`String.fromCharCode`** 使用内联 ASCII 快速路径——每次典型扫描可减少约 1 M 次分发。
- **`decodeURI` / `decodeURIComponent`** 使用 12 字符的 `%XX%XX%XX%XX` 超快速路径，并通过 `hex2bin` 批量解码。
- **`Object.keys` / `Object.values` / `Object.entries`** 直接访问属性映射。
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** 原生迭代底层的 PHP 数组。

每条快速路径都有“形态检查”作为门控——如果接收者不符合预期形态（e.g，例如底层不是普通 PHP `string` 的 TypedArray 视图），VM 就会回退到符合规范的慢速路径。

## 此实现不做什么

从 V8 的含义来看，字节码 VM **不是 JIT**。它不会为每个函数生成原生机器码，也不会基于观察到的类型进行推测（IC 的单态原型标识检查除外）。函数边界内联通过自定义调用栈来近似实现（每次 JS 调用不增加额外的 PHP 帧），而不是将被调用函数的函数体降级并写入调用方字节码。

JsToPhp 转译器是引擎中最接近 JIT 的部分：它为热点函数体生成 PHP 源代码，再由 PHP 8.5 的跟踪 JIT 编译为原生代码。JsToPhp 通过 `eval()` 执行，因此需要承担一次性编译开销，而且跟踪 JIT 必须像发现用户代码一样发现该函数。跳过 `eval()` 步骤并直接生成 opcache 操作码可以进一步缩小差距——参见 [基准测试：路线图](https://phasis.inline0.dev/zh/docs/advanced/benchmarks#roadmap)。

## 基准测试

`bench/microbench.js` 会运行十多个小型基准测试（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）。中位墙钟时间：

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

每次 `bench` 工作流运行后，当前数据都会提交到 `BENCH.md`。`bench` 和 test262 矩阵工作流均由人工启动。
