架构
Phasis 是一个四阶段树遍历器,并带有机会式字节码 VM。数据流如下:
JS text
│
▼
┌──────────┐
│ Lexer │ src/Lexer/
└────┬─────┘
▼ tokens
┌──────────┐
│ Parser │ src/Parser/
└────┬─────┘
▼ AST nodes
┌──────────────┐
│ Interpreter │ src/Runtime/
│ + Bytecode │ src/Bytecode/
│ VM │
└────┬─────────┘
▼ JsValue
PHP host
每个阶段都是一个 PHP 类,对外接口精简,由下一阶段使用。没有链接步骤——解析会生成 AST,解释器直接遍历 AST。
词法分析器
src/Lexer/Lexer.php 将源文本转换为 Token 实例流。每个令牌都是只读结构体,包含类型(TokenType 枚举,约 80 种情况)、值、行和列。
词法分析器处理:
- 数字字面量——十进制、十六进制(
0x)、八进制(0o)、二进制(0b)、指数形式和数字分隔符。 - 字符串字面量——单引号和双引号、所有转义序列、带嵌套表达式的模板字面量。
- 正则表达式字面量——完整的
/pattern/flags令牌化,并能判断/应解析为正则表达式还是除法。 - 标识符、关键字、标点符号。
- 注释(行注释和块注释),作为附属信息保留。
- Unicode 源——将 UTF-8 字节视为 JS UTF-16 源。
输出是一个惰性生成令牌的生成器。解析器通过单令牌前瞻来消费这些令牌。
解析器
src/Parser/Parser.php 是手写的 Pratt 解析器。表达式优先级定义在 src/Parser/Precedence.php 中。语句则使用独立的递归下降解析器。
它根据 src/Ast/ 生成不可变 AST 节点。每个节点都带有源代码位置,供堆栈跟踪和错误报告使用。
值得注意的结构:
- 表达式:
Literal、Identifier、BinaryExpression、UnaryExpression、AssignmentExpression、CallExpression、MemberExpression、ArrowFunction、ObjectExpression、ArrayExpression、ConditionalExpression、TemplateLiteral、SpreadElement、NewExpression、ThisExpression、SequenceExpression、TaggedTemplate、ClassExpression、YieldExpression、AwaitExpression。 - 语句:
BlockStatement、IfStatement、ForStatement、ForInStatement、ForOfStatement、WhileStatement、DoWhileStatement、SwitchStatement、ReturnStatement、ThrowStatement、TryStatement、BreakStatement、ContinueStatement、ExpressionStatement。 - 声明:
VariableDeclaration、FunctionDeclaration、ClassDeclaration、ImportDeclaration。 - 模式(解构):
ArrayPattern、ObjectPattern、RestElement、AssignmentPattern。
自动分号插入是在解析器中(而不是词法分析器中)根据规范实现的。
解释器
src/Runtime/Interpreter.php 是 AST 遍历器。它访问每个节点并返回一个 JsValue(对于改变控制流的语句,则返回 Completion)。
关键的辅助类:
Environment(src/Runtime/Environment.php)——词法作用域链。每次函数调用都会创建一个指向其词法父级的新环境。var会提升到函数作用域;let/const是块级作用域,并有 TDZ。CallStack(src/Runtime/CallStack.php)——带深度限制的显式栈。函数和方法调用会压入栈帧;异常会展开这些栈帧。Completion(src/Runtime/Completion.php)——由normal、return、throw、break、continue组成的判别联合类型,另含可选的值和标签。Reference(src/Runtime/Reference.php)——规范中的 Reference 类型,用于赋值和delete中的左值求值。
值
JavaScript 值是在 src/Value/ 下的 PHP 对象。每个值都实现 JsValue 接口,并带有自己的类型转换方法(toNumber()、toString()、toBoolean())。
| 类 | JS 类型 |
|---|---|
JsUndefined |
undefined(单例) |
JsNull |
null(单例) |
JsBoolean |
true / false |
JsNumber |
IEEE 754 双精度;保留 NaN、±Infinity、±0 |
JsString |
UTF-16 字符串语义 |
JsObject |
带属性映射和原型链的基础对象 |
JsArray |
扩展 JsObject,并跟踪长度 |
JsFunction |
带有捕获的 Environment 的闭包 |
JsSymbol |
Symbol 原始值 |
JsBigInt |
通过 bcmath 实现任意精度 |
JsProxy |
全部 13 个陷阱,且可撤销 |
JsPromise |
同步执行器 + 微任务队列 |
JsGenerator |
双模式:帧快照模式(字节码编译的主体;参见 字节码 VM § 帧快照生成器),并在 yield*、带 yield 的 try 语句和异步生成器中使用 PHP Fiber 回退方案 |
JsMap、JsSet、JsWeakMap、JsWeakSet、JsWeakRef、JsFinalizationRegistry |
集合 |
JsArrayBuffer、JsSharedArrayBuffer、JsDataView、JsTypedArray |
二进制数据 |
数字处理是历史问题中占比最大的一类:必须保证 NaN !== NaN 为 true,-0 === 0 也必须为 true;ToNumber 的边缘情况需要精确到位级处理。Spec/TypeConversion 类逐字实现了规范算法,并经过全面测试。
内置库
src/BuiltIn/ 提供所有标准库对象:Array、String、Object、Math、JSON、Date、RegExp、Map、Set、Promise、Proxy、Reflect、Symbol、BigInt、TypedArray、Temporal、Intl、console,以及 Web Platform / Fetch 套件(URL、Headers、Request、Response、Blob、FormData、完整 Streams、EventTarget、AbortController/AbortSignal、DOMException、structuredClone、performance、TextEncoder/TextDecoder、atob/btoa)、crypto + SubtleCrypto、WebSocket、XMLHttpRequest,以及事件循环(setTimeout、setInterval、queueMicrotask、AsyncContext)。
每个内置对象都是类上的一个静态 install() 方法。构造时,Engine 会按依赖顺序调用它们。每个方法都会定义其原型链,将方法安装为包装在 PHP 闭包中的 JsFunction 实例,并将构造函数作为全局对象公开。
PHP↔JS 桥接
跨边界的值转换分布在两个位置:
Phasis\Interop\PhpToJs::convert()——将 PHP 值转换为JsValue,用于setGlobal()、call()参数和宿主函数返回值。PHP 可调用对象会变成一个JsFunction,其主体会调用该对象。PhpToJs::toPhp()——将JsValue转换为 PHP,用于宿主函数的参数。Engine::toPhp()(私有)——将JsValue转换为 PHP,用于eval()和call()的结果。
没有对象桥接:不可调用的 PHP 对象会转换为 undefined(问题 #18)。JsToPhp 类在 src/Bytecode/ 中,与互操作无关;它是下文介绍的转译器。转换规则详见 值转换。
错误处理
未捕获的 JS throw 会变成 PHP Phasis\Exceptions\JsThrowable;引擎自身抛出的错误会以 Phasis\Exceptions\TypeError、RangeError 或 ReferenceError 的形式呈现,这些都是 Phasis\Exceptions\RuntimeError 的子类。在引擎内部,JS try / catch 会捕获 JsThrowable、任何 RuntimeError 和 Phasis\Exceptions\SyntaxError。其他 PHP 异常(包括宿主函数抛出的异常)不会转换,而是直接传播给 PHP 调用方(问题 #20)。
有两个限制:调用栈上限(CallStack::maxDepth,4,096 个栈帧,以及针对转译闭包的 8,192 帧保护)和 maxLoopIterations,通过 Engine::setLimit() 设置,且仅由树遍历器强制执行(问题 #19)。两者都会抛出 Phasis\Exceptions\InternalError。字符串长度、输出大小或执行时间均无限制。
字节码 VM
热点函数会以两种方式降低。JsToPhp(在 src/Bytecode/ 中)会为数值运算或大量使用局部变量的主体生成 PHP 闭包,PHP 的跟踪式 JIT 可以将其编译为本机代码;该流水线会降低 new Ctor(args)、obj.method(args) 和 this.member 读取,足以让典型的循环内类方法模式完全绕过 VM。其他代码则会编译为由 VM::execute 执行的扁平字节码,并使用自定义调用栈,让每次 JS 调用都留在单个 PHP 帧中,同时为原型方法读取提供按 PC 设置的内联缓存。简单的生成器主体也会编译为字节码,此时 Op::YIELD 会捕获堆帧快照,而不是跨越 PHP Fiber。
编译器无法降低的结构(e.g、with、带有重新绑定 catch 的 try、yield*、异步生成器)会回退到树遍历器。完整机制请参阅 字节码 VM。