跳转到正文

架构

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。