アーキテクチャ
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をご覧ください。