コンテンツへスキップ

アーキテクチャ

Phasisは、機会に応じてバイトコードVMを使う4段階のツリーウォーカーです。データの流れは次のとおりです。

コード
JS text
  │
  ▼
┌──────────┐
│  Lexer   │  src/Lexer/
└────┬─────┘
     ▼ tokens
┌──────────┐
│  Parser  │  src/Parser/
└────┬─────┘
     ▼ AST nodes
┌──────────────┐
│ Interpreter  │  src/Runtime/
│  + Bytecode  │  src/Bytecode/
│      VM      │
└────┬─────────┘
     ▼ JsValue
PHP host

各段階はPHPクラスで構成され、次の段階が利用する小さなインターフェースを備えています。リンク処理はありません。パースでASTが生成され、インタープリターが直接走査します。

字句解析器

src/Lexer/Lexer.phpはソーステキストをTokenインスタンスのストリームに変換します。各トークンは読み取り専用の構造体で、型(TokenType列挙型、約80種類)、値、行、列を持ちます。

字句解析器が処理するもの:

  • 数値リテラル - 10進数、16進数(0x)、8進数(0o)、2進数(0b)、指数表記、数値区切り。
  • 文字列リテラル - シングルクォートとダブルクォート、すべてのエスケープシーケンス、式を入れ子にできるテンプレートリテラル。
  • 正規表現リテラル - 完全な/pattern/flagsトークン化に対応し、/を正規表現として扱うか除算として扱うかを判別します。
  • 識別子、キーワード、句読点。
  • コメント(行コメントとブロックコメント)。トリビアとして保持されます。
  • Unicodeソース - UTF-8バイトをJSのUTF-16ソースとして解釈します。

出力はトークンを遅延生成するジェネレーターです。パーサーは1トークン先読みでこれを読み込みます。

パーサー

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)— 代入やdeleteにおける左辺評価のための、仕様上のReference型です。

値

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 2つのモードに対応:フレームスナップショット(バイトコードにコンパイルされた本体。詳細はバイトコードVM § フレームスナップショットジェネレーター)と、yield*、try-with-yield、非同期ジェネレーター向けのPHP Fiberフォールバック
JsMap、JsSet、JsWeakMap、JsWeakSet、JsWeakRef、JsFinalizationRegistry コレクション
JsArrayBuffer、JsSharedArrayBuffer、JsDataView、JsTypedArray バイナリデータ

数値処理は、過去のバグの最大の原因です。NaN !== NaNと-0 === 0が真でなければならず、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ブリッジ

境界を越える値変換は、次の2か所で行われます。

  • Phasis\Interop\PhpToJs::convert() — PHP値からJsValueへの変換です。setGlobal()、call()の引数、ホスト関数の戻り値で使われます。PHPの呼び出し可能オブジェクトは、その本体がそれを呼び出すJsFunctionになります。
  • PhpToJs::toPhp() — ホスト関数の引数について、JsValueからPHPに変換します。
  • Engine::toPhp()(プライベート)— eval()とcall()の結果について、JsValueからPHPに変換します。

オブジェクトブリッジはありません。呼び出し可能でないPHPオブジェクトはundefinedに変換されます(issue #18)。src/Bytecode/のJsToPhpクラスは相互運用とは無関係で、後述するトランスパイラーです。変換規則は値の変換に記載されています。

エラー処理

捕捉されなかったJSのthrowはPHPPhasis\Exceptions\JsThrowableになります。エンジン自体が発生させるエラーはPhasis\Exceptions\TypeError、RangeError、ReferenceErrorとして表面化し、いずれもPhasis\Exceptions\RuntimeErrorのサブクラスです。エンジン内では、JSのtry / catchがJsThrowable、あらゆるRuntimeError、およびPhasis\Exceptions\SyntaxErrorを捕捉します。ホスト関数が投げたものを含む、その他のPHP例外は変換されず、PHP呼び出し元へそのまま伝播します(issue #20)。

制限は2つあります。呼び出しスタックの上限(CallStack::maxDepth、4,096フレームに加えて、トランスパイルされたクロージャーには8,192フレームのガード)と、maxLoopIterationsです。Engine::setLimit()で設定し、ツリーウォーカーのみが適用します(issue #19)。どちらもPhasis\Exceptions\InternalErrorを投げます。文字列長、出力サイズ、実行時間の制限はありません。

バイトコードVM

ホット関数は2通りに低レベル化されます。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をご覧ください。