V8 Torque ユーザーマニュアル
V8 Torqueは、V8プロジェクトに貢献する開発者が、VMの意図的な変更に焦点を当て、実装に関係のない詳細に気を取られることなく、変更を表現できるようにする言語です。この言語は、ECMAScript仕様を直接V8に実装するのを簡単にするために十分にシンプルでありながら、特定のオブジェクト形状のテストに基づく高速経路の作成など、堅牢な方法で低レベルのV8最適化トリックを表現するのに十分に強力に設計されています。
Torqueは、V8エンジニアとJavaScript開発者に馴染みがあるもので、TypeScriptのような構文を兼ね備えており、V8コードの記述や理解を容易にします。また、コードスタブアセンブラ(CodeStubAssembler)で既に共通している概念を反映した構文や型を組み込んでいます。強力な型システムと構造化された制御フローにより、Torqueは設計段階からの正確さを保証します。Torqueの表現力は、現在V8のビルトインで見つかるほぼすべての機能を表現するのに十分です。また、C++で記述されたCodeStubAssemblerビルトインやmacroとの相互運用性を共有しており、Torqueコードが手書きのCSA機能を使用することもその逆も可能です。
Torqueは、高レベルでセマンティックリッチなV8の実装の断片を表現するための言語構造を提供し、Torqueコンパイラがこれらの断片をCodeStubAssemblerを使用して効率的なアセンブリコードに変換します。Torqueの言語構造とコンパイラのエラーチェックは、CodeStubAssemblerを直接使用する従来の方法では面倒で誤りがちだった正確性を保証する方法を提供します。従来、CodeStubAssemblerを使用して最適なコードを書くには、V8エンジニアが多くの専門知識を頭に入れ、微妙な落とし穴を回避する必要がありました。この知識の多くは文書化されていなかったため、効率的なビルトインを書くための学習曲線は急でした。また、必要な知識を習得していても、非直感的で監視されていない落とし穴により正確性やセキュリティバグがしばしば発生しました。Torqueを使用することで、これらの落とし穴の多くが回避され、自動的にTorqueコンパイラによって認識されるようになります。
入門
Torqueで記述された大部分のソースコードは、V8リポジトリのsrc/builtinsディレクトリに*.tqの拡張子でチェックインされています。V8のヒープに割り当てられたクラスのTorque定義は、C++定義と一緒に.tqファイルに存在し、src/objects内の対応するC++ファイルと同じ名前になっています。実際のTorqueコンパイラはsrc/torque以下にあります。Torque機能のテストは、test/torque、test/cctest/torque、およびtest/unittests/torque以下にチェックインされています。
Torque言語を体験するために、「Hello World!」を出力するV8ビルトインを書いてみましょう。これを行うには、Torqueのmacroをテストケース内に追加し、それをcctestテストフレームワークから呼び出します。
test/torque/test-torque.tqファイルを開き、次のコードを末尾に追加します(ただし、最後の閉じ括弧}の前に記述します):
@export
macro PrintHelloWorld(): void {
Print('Hello world!');
}
次に、test/cctest/torque/test-torque.ccを開き、新しいTorqueコードを使用してコードスタブを構築する次のテストケースを追加します:
TEST(HelloWorld) {
Isolate* isolate(CcTest::InitIsolateOnce());
CodeAssemblerTester asm_tester(isolate, JSParameterCount(0));
TestTorqueAssembler m(asm_tester.state());
{
m.PrintHelloWorld();
m.Return(m.UndefinedConstant());
}
FunctionTester ft(asm_tester.GenerateCode(), 0);
ft.Call();
}
次にcctest実行可能ファイルをビルドします、最後にcctestテストを実行して「Hello world」を出力します:
$ out/x64.debug/cctest test-torque/HelloWorld
Hello world!
Torqueがコードを生成する仕組み
Torqueコンパイラは、直接的に機械コードを生成するのではなく、V8の既存のCodeStubAssemblerインターフェースを呼び出すC++コードを生成します。CodeStubAssemblerはTurboFanコンパイラのバックエンドを使用して効率的なコードを生成します。そのため、Torqueのコンパイルには複数のステップが必要です:
-
gnビルドは、まずTorqueコンパイラを実行します。それにより、すべての*.tqファイルが処理されます。それぞれのTorqueファイルpath/to/file.tqは、以下のファイルを生成します:path/to/file-tq-csa.ccとpath/to/file-tq-csa.hに生成されたCSAマクロを含む。- クラス定義を含む対応するヘッダー
path/to/file.hにインクルードされるpath/to/file-tq.inc。 - クラス定義のC++アクセサを含む対応するインラインヘッダー
path/to/file-inl.hにインクルードされるpath/to/file-tq-inl.inc。 - 生成されたヒープバリファイアやプリンターなどを含む
path/to/file-tq.cc。
Torqueコンパイラは、V8ビルドで使用される他のさまざまな
.hファイルも生成します。 -
gnビルドは、ステップ1で生成された-csa.ccファイルをコンパイルしてmksnapshot実行ファイルを生成します。 -
mksnapshotが実行されると、V8のすべてのビルトインが生成され、スナップショットファイルにパッケージングされます。それには、Torqueで定義されたものや、Torqueで定義された機能を使用するその他のビルトインも含まれます。 -
残りのV8がビルドされます。Torqueで作成されたすべてのビルトインは、スナップショットファイルを通じてV8内でアクセス可能になります。それらは他のビルトイン同様に呼び出すことができます。さらに、
d8またはchrome実行ファイルには、クラス定義に関連する生成されたコンパイルユニットが直接含まれます。
視覚的には、ビルドプロセスは次のように見えます:
Torqueツール
Torqueには基本的なツールと開発環境サポートが用意されています。
- Torque用の Visual Studio Codeプラグイン があり、カスタム言語サーバーを使って定義への移動機能などを提供します。
.tqファイルを変更した後に使用すべきフォーマットツールもあります:tools/torque/format-torque.py -i <filename>
Torqueを含むビルドのトラブルシューティング
これを知る必要がある理由は? Torqueファイルが機械コードに変換される各プロセスを理解することは重要です。なぜなら、Torqueをスナップショットに埋め込まれたバイナリコードに変換する各段階で、さまざまな問題(およびバグ)が発生する可能性があるからです。
- Torqueコード(つまり
.tqファイル)に構文エラーや意味エラーがある場合、Torqueコンパイラが失敗します。この段階でV8ビルドは中止され、ビルドの後半部分で発見される可能性のある他のエラーは表示されません。 - Torqueコードの構文が正しく、Torqueコンパイラの厳密な意味チェックに合格したとしても、
mksnapshotのビルドが失敗する可能性があります。これは主に、.tqファイルで提供される外部定義に矛盾がある場合に発生します。Torqueコードでexternキーワードでマークされた定義は、必要な機能の定義がC++にあることをTorqueコンパイラに知らせます。現在のところ、.tqファイルのextern定義と、それらが参照するC++コードとの結合は緩やかであり、その結合はTorqueコンパイル時には検証されません。extern定義が一致しない(または微妙な場合には機能を誤認させる)と、code-stub-assembler.hヘッダーファイルや他のV8ヘッダーでアクセスする機能に影響し、mksnapshotのC++ビルドが失敗します。 mksnapshotが正常にビルドされても、実行時に失敗する場合があります。例えば、Torqueのstatic_assertがTurbofanによって検証できないため、生成されたCSAコードをTurbofanがコンパイルできない場合があります。また、スナップショット作成時に実行されるTorque提供のビルトインにバグがある可能性もあります。例えば、Torqueで作成されたビルトインであるArray.prototype.spliceは、JavaScriptのスナップショット初期化プロセスの一部として、デフォルトのJavaScript環境を設定するために呼び出されます。その実装にバグがあると、mksnapshotは実行中にクラッシュします。mksnapshotがクラッシュした場合は、--gdb-jit-fullフラグを指定して呼び出すと、追加のデバッグ情報が生成され、例えばTorque生成のビルトイン名がgdbスタックトレースに表示されるなど、有用なコンテキストが得られることがあります。- もちろん、Torque作成のコードが
mksnapshotを通過したとしても、それがバグを含んだりクラッシュしたりする可能性があります。torque-test.tqやtorque-test.ccにテストケースを追加することで、自分のTorqueコードが期待通りに動作することを確認するのが良い方法です。もしTorqueコードが結果的にd8やchromeでクラッシュする場合、再び--gdb-jit-fullフラグが非常に役立ちます。
constexpr: コンパイル時 vs. 実行時
Torqueビルドプロセスを理解することは、Torque言語のコア機能である constexpr を理解する上でも重要です。
Torqueは、Torqueコード内の式を実行時に評価することを許可します(つまり、JavaScriptを実行する一環としてV8のビルトインが実行されるとき)。しかし、それだけでなく、式をコンパイル時に評価することも可能です(つまり、Torqueビルドプロセスの一部として、V8ライブラリや d8 実行ファイルが作成される前に)。
Torqueは、式をビルド時に評価する必要があることを示すためにconstexprキーワードを使用します。その使用法はC++のconstexprに多少類似しています。C++からconstexprキーワードとその一部の構文を借りるだけでなく、Torqueでは同様に、コンパイルタイムでの評価とランタイムでの評価の区別を示すためにconstexprを使用します。
ただし、Torqueのconstexprのセマンティクスにはいくつか微妙な違いがあります。C++では、constexpr式はC++コンパイラによって完全に評価される可能性があります。Torqueでは、constexpr式はTorqueコンパイラによって完全には評価されず、代わりにC++の型、変数、および式にマップされます。それらはmksnapshotの実行時に完全に評価される必要があります。Torqueライターの視点では、constexpr式はランタイムで実行されるコードを生成しないため、その意味でコンパイルタイムのものと考えられますが、技術的にはTorqueに外部にあるC++コードによって評価され、mksnapshotが実行されるものです。したがって、Torqueでは、constexprは基本的に「mksnapshot時」を意味し、「コンパイル時」を意味するわけではありません。
ジェネリックと組み合わせることで、constexprはV8の開発者が事前に予想できる特定の少数の詳細が異なる、非常に効率的な特殊化されたビルトインを多数生成するために使用できる強力なTorqueツールです。
ファイル
Torqueコードは個々のソースファイルにパッケージ化されています。各ソースファイルは一連の宣言で構成され、これらの宣言は名前空間宣言で任意にラップされ、宣言の名前空間を分離することができます。文法の説明はおそらく最新ではありません。真実のソースはTorqueコンパイラの文法定義にあり、コンテキストフリー文法ルールを使用して記述されています。
Torqueファイルは宣言のシーケンスです。可能な宣言はtorque-parser.ccにリストされています。
名前空間
Torqueの名前空間は独立した名前空間で宣言を行うことができます。C++の名前空間に似ています。他の名前空間で自動的に表示されない宣言を作成することができます。名前空間は入れ子にすることが可能で、入れ子の名前空間内の宣言はそれを含む名前空間の宣言に資格なしでアクセスできます。名前空間宣言で明示的に指定されていない宣言は、すべての名前空間で表示される共有グローバルデフォルト名前空間に配置されます。名前空間は再開することができ、複数のファイルにまたがって定義することができます。
例:
macro IsJSObject(o: Object): bool { … } // デフォルト名前空間内
namespace array {
macro IsJSArray(o: Object): bool { … } // array名前空間内
};
namespace string {
// …
macro TestVisibility() {
IsJsObject(o); // OK, グローバル名前空間がここで見える
IsJSArray(o); // エラー、この名前空間内では見えない
array::IsJSArray(o); // OK, 明示的な名前空間指定
}
// …
};
namespace array {
// OK, 名前空間が再開されました。
macro EnsureWriteableFastElements(array: JSArray){ … }
};
宣言
型
Torqueは強く型付けされています。その型システムは、それが提供する多くのセキュリティおよび正確性の保証の基礎です。
多くの基本的な型に対して、Torqueは実際にはそれらについて非常識には多くの知識を持っていません。代わりに、多くの型は明示的な型マッピングを通じてCodeStubAssemblerおよびC++型とゆるく結合されており、そのマッピングの厳格性をC++コンパイラに依存しています。そのような型は抽象型として実現されています。
抽象型
Torqueの抽象型はC++コンパイル時およびCodeStubAssemblerランタイム値に直接マップされます。その宣言は名前とC++型との関係を指定します:
AbstractTypeDeclaration :
type IdentifierName ExtendsDeclaration opt GeneratesDeclaration opt ConstexprDeclaration opt
ExtendsDeclaration :
extends IdentifierName ;
GeneratesDeclaration :
generates StringLiteral ;
ConstexprDeclaration :
constexpr StringLiteral ;
IdentifierNameは抽象型の名前を指定し、ExtendsDeclarationは、宣言された型が派生している型をオプションで指定します。GeneratesDeclarationは、CodeStubAssemblerコードでその型のランタイム値を含むために使用されるC++のTNode型に対応する文字列リテラルをオプションで指定します。ConstexprDeclarationは、ビルド時(mksnapshot時)評価のためのTorque型に対応するC++型を指定する文字列リテラルです。
base.tqからTorqueの31ビットと32ビットの符号付き整数型の例を以下に示します:
type int32 generates 'TNode<Int32T>' constexpr 'int32_t';
type int31 extends int32 generates 'TNode<Int32T>' constexpr 'int31_t';
ユニオン型
ユニオン型は値がいくつかの可能な型のいずれかに属することを表します。タグ付き値に対してのみユニオン型を許可しています。これは、マップポインタを使用してランタイムで区別できるためです。例えば、JavaScriptの数値はSmi値または割り当てられたHeapNumberオブジェクトです。
type Number = Smi | HeapNumber;
共用型は以下の等式を満たします:
A | B = B | AA | (B | C) = (A | B) | CA | B = AただしBがAのサブタイプである場合
タグ付き型のみから共用型を形成することが許可されています。なぜなら、タグ付けされていない型では実行時に区別することができないためです。
共用型をCSAにマッピングする場合、共用型内のすべての型に共通する最も具体的なスーパタイプが選択されます。ただし、Number および Numeric については対応するCSA共用型にマッピングされます。
クラスタイプ
クラスタイプを使用すると、TorqueコードからV8 GCヒープ上の構造化されたオブジェクトを定義、割り当て、操作することが可能になります。それぞれのTorqueクラスタイプはC++コード内のHeapObjectのサブクラスに対応する必要があります。V8のC++実装とTorque実装の間で冗長なコードを維持するコストを最小限に抑えるために、Torqueクラスタイプの定義は必要に応じてC++のオブジェクトアクセスコードを生成するのに使用されます。これにより、C++とTorqueの同期を手動で取る手間が削減されます。
ClassDeclaration :
ClassAnnotation* extern opt transient opt class IdentifierName ExtendsDeclaration opt GeneratesDeclaration opt {
ClassMethodDeclaration*
ClassFieldDeclaration*
}
ClassAnnotation :
@doNotGenerateCppClass
@generateBodyDescriptor
@generatePrint
@abstract
@export
@noVerifier
@hasSameInstanceTypeAsParent
@highestInstanceTypeWithinParentClassRange
@lowestInstanceTypeWithinParentClassRange
@reserveBitsInInstanceType ( NumericLiteral )
@apiExposedInstanceTypeValue ( NumericLiteral )
ClassMethodDeclaration :
transitioning opt IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt LabelsDeclaration opt StatementBlock
ClassFieldDeclaration :
ClassFieldAnnotation* weak opt const opt FieldDeclaration;
ClassFieldAnnotation :
@noVerifier
@if ( Identifier )
@ifnot ( Identifier )
FieldDeclaration :
Identifier ArraySpecifier opt : Type ;
ArraySpecifier :
[ Expression ]
クラスの例:
extern class JSProxy extends JSReceiver {
target: JSReceiver|Null;
handler: JSReceiver|Null;
}
extern は、このクラスがTorqueでのみ定義されたものではなく、C++で定義されていることを示します。
クラス内のフィールド宣言は、CodeStubAssemblerから使用可能なフィールドのゲッターとセッターを暗黙的に生成します。例えば:
// In TorqueGeneratedExportedMacrosAssembler:
TNode<HeapObject> LoadJSProxyTarget(TNode<JSProxy> p_o);
void StoreJSProxyTarget(TNode<JSProxy> p_o, TNode<HeapObject> p_v);
前述のように、Torqueで定義されるフィールドはC++コードを生成し、重複するアクセサやヒープ訪問コードを排除します。JSProxyの手書き定義は、以下のように生成されたクラステンプレートを継承する必要があります:
// In js-proxy.h:
class JSProxy : public TorqueGeneratedJSProxy<JSProxy, JSReceiver> {
// Torqueによって生成された内容以外に必要なものをここに記述...
// 最後に、パブリック/プライベートの設定に影響するため:
TQ_OBJECT_CONSTRUCTORS(JSProxy)
}
// In js-proxy-inl.h:
TQ_OBJECT_CONSTRUCTORS_IMPL(JSProxy)
生成されたクラスはキャスト関数、フィールドアクセサ関数、およびフィールドオフセット定数(この場合、kTargetOffset や kHandlerOffset など)を提供します。これらはクラスの先頭からの各フィールドのバイトオフセットを表します。
クラスタイプの注釈
一部のクラスでは、上記の例に示した継承パターンを使用できない場合があります。そのような場合、クラスは @doNotGenerateCppClass を指定し、直接スーパークラスタイプを継承し、フィールドオフセット定数用のTorque生成マクロを含めることができます。このようなクラスは独自にアクセサやキャスト関数を実装する必要があります。このマクロを使用する例は以下の通りです:
class JSProxy : public JSReceiver {
public:
DEFINE_FIELD_OFFSET_CONSTANTS(
JSReceiver::kHeaderSize, TORQUE_GENERATED_JS_PROXY_FIELDS)
// クラスの他の部分は省略...
}
@generateBodyDescriptor を追加すると、Torqueは生成されたクラス内にBodyDescriptorを生成します。これはガベージコレクタがオブジェクトをどのように訪問すべきかを表します。それ以外の場合、C++コードは独自のオブジェクト訪問を定義するか、既存のパターンを使用する必要があります(例えば、Structを継承し、STRUCT_LISTにクラスを含めると、クラスはタグ付き値のみを含むとみなされます)。
@generatePrint 注釈が追加されると、ジェネレータはTorqueレイアウトで定義されたフィールド値を出力するC++関数を実装します。JSProxyの例を使用すると、シグネチャは void TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyPrint(std::ostream& os) となり、これはJSProxyが継承することができます。
また、Torqueコンパイラはすべてのexternクラスに対して検証コードを生成しますが、クラスが@noVerifier注釈を使用してオプトアウトする場合は除きます。例えば、上記のJSProxyクラス定義は、Torqueタイプ定義に従ってフィールドが有効であることを検証するC++メソッドvoid TorqueGeneratedClassVerifiers::JSProxyVerify(JSProxy o, Isolate* isolate)を生成します。また、生成されたクラスTorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyVerifyにも対応する関数を生成し、これがTorqueGeneratedClassVerifiersの静的関数を呼び出します。クラスに追加の検証を加えたい場合(例えば、数値の受け入れ可能な範囲、またはフィールドbarが非nullの場合にフィールドfooがtrueである必要があるなど)、C++クラスにDECL_VERIFIER(JSProxy)を追加してそれをsrc/objects-debug.ccで実装します。そのようなカスタム検証の最初のステップは、生成された検証器を呼び出すことであるべきです。例えば、TorqueGeneratedClassVerifiers::JSProxyVerify(*this, isolate);。 (これらの検証器をGCの前後で実行するには、v8_enable_verify_heap = trueでビルドし、--verify-heapで実行してください。)
@abstractは、クラス自体がインスタンス化されず、それ自身のインスタンスタイプを持たないことを示します。論理的にクラスに属するインスタンスタイプは、派生クラスのインスタンスタイプです。
@export注釈は、Torqueコンパイラが具体的なC++クラス(上記例のJSProxyなど)を生成するようにします。これは明らかに、Torque生成コードによって提供される以上のC++機能を追加したくない場合にのみ有用です。externと併用することはできません。Torque内でのみ定義され使用されるクラスの場合、externも@exportも使用しないのが最も適切です。
@hasSameInstanceTypeAsParentは、親クラスと同じインスタンスタイプを持つが、一部のフィールドの名前を変更したり、別のマップを持ったりする可能性があるクラスを示します。このような場合、親クラスは抽象的ではありません。
@highestInstanceTypeWithinParentClassRange、@lowestInstanceTypeWithinParentClassRange、@reserveBitsInInstanceType、および@apiExposedInstanceTypeValueの各注釈は、インスタンスタイプの生成に影響します。通常はこれらを無視しても問題ありません。Torqueは、v8::internal::InstanceType列挙内のすべてのクラスに対してコンパイル時にユニークな値を割り当て、V8がJSヒープ内の任意のオブジェクトの型を実行時に判断できるようにします。Torqueのインスタンスタイプの割り当ては大半のケースで十分ですが、一部のケースでは、特定のクラスのインスタンスタイプをビルド間で安定させたい場合、または親クラスのインスタンスタイプ範囲の始点または終点に配置したい場合、Torque外部で定義可能な予約値の範囲が必要な場合があります。
クラスフィールド
上記の例のような単純な値だけでなく、クラスフィールドにインデックス付きデータを含めることもできます。以下はその例です:
extern class CoverageInfo extends HeapObject {
const slot_count: int32;
slots[slot_count]: CoverageInfoSlot;
}
これは、CoverageInfoのインスタンスがslot_countのデータに基づいてさまざまなサイズであることを意味します。
C++とは異なり、Torqueはフィールド間に自動的にパディングを追加しません。代わりに、フィールドが正しく整列されていない場合はエラーを発生させます。また、Torqueは強いフィールド、弱いフィールド、およびスカラーフィールドが同じカテゴリの他のフィールドと一緒にフィールド順に配置されることを要求します。
constは、フィールドが実行時に変更できない(少なくとも簡単には変更できない)ことを意味します。Torqueでは、これを試みるとコンパイルが失敗します。これは、長さフィールドには良い考えです。これらは慎重にリセットされるべきであり、解放されたスペースを解放する必要がある場合もあり、マーキングスレッドとのデータ競合を引き起こす可能性があります。
実際、Torqueはインデックス付きデータに使用される長さフィールドがconstであることを要求します。
weakはフィールド宣言の先頭にあると、そのフィールドがカスタムの弱参照であることを意味します。ただし、弱フィールドに対するMaybeObjectタグ付けメカニズムとは異なります。
さらに、weakはkEndOfStrongFieldsOffsetやkStartOfWeakFieldsOffsetといった定数の生成に影響します。これは一部のカスタムBodyDescriptorで使用されるレガシー機能であり、現在もまだweakとマークされたフィールドをまとめて配置する必要があります。TorqueがすべてのBodyDescriptorを生成できるようになったら、このキーワードを削除する予定です。
フィールドに格納されるオブジェクトがMaybeObject形式の弱参照(2番目のビットがセットされているもの)である可能性がある場合は、型にWeak<T>を使用すべきであり、weakキーワードを使用してはいけません。この規則に対する例外はいくつか存在します。例えば、Mapのフィールドのように、強参照型と弱参照型の両方を含む場合や、弱セクションへの含まれるためにweakとしてもマークされている場合:
weak transitions_or_prototype_info: Map|Weak<Map>|TransitionArray|
PrototypeInfo|Smi;
@ifと@ifnotは、特定のビルド構成で含まれるべきフィールドをマークしますが、他の構成には含まれません。これらはsrc/torque/torque-parser.ccにあるBuildFlagsのリストから値を受け取ります。
Torque外で完全に定義されたクラス
一部のクラスはTorqueで定義されていませんが、Torqueはインスタンスタイプを割り当てる責任があるため、すべてのクラスを知っている必要があります。この場合、クラスはボディなしで宣言することができます。この場合、Torqueはインスタンスタイプ以外のものを生成しません。例:
extern class OrderedHashMap extends HashTable;
シェイプ
shapeを定義することは、classを定義するのと同様ですが、classの代わりにキーワードshapeを使用します。shapeは、JSObjectのサブタイプで、インオブジェクトプロパティの時点での配置を表します(仕様上ではこれは「データプロパティ」で、「内部スロット」ではありません)。shapeは自身のインスタンスタイプを持ちません。特定のシェイプを持つオブジェクトは辞書モードに入る可能性があり、プロパティを別のバックストアに移動するため、いつでもそのシェイプを失うことがあります。
構造体
structはデータのコレクションをまとめて簡単に受け渡すためのものです(Structという名前のクラスとは完全に無関係です)。クラスのように、データに対して操作するマクロを含めることができます。クラスとは異なり、ジェネリックもサポートしています。その構文はクラスに似ています:
@export
struct PromiseResolvingFunctions {
resolve: JSFunction;
reject: JSFunction;
}
struct ConstantIterator<T: type> {
macro Empty(): bool {
return false;
}
macro Next(): T labels _NoMore {
return this.value;
}
value: T;
}
Structのアノテーション
@exportとマークされた任意のstructは、生成されたファイルgen/torque-generated/csa-types.hに予測可能な名前で含まれます。その名前にはTorqueStructが先頭につけられるため、PromiseResolvingFunctionsはTorqueStructPromiseResolvingFunctionsになります。
Structのフィールドはconstとしてマークすることができ、これは書き込まれないことを意味します。ただし、struct全体が上書きされることは可能です。
Structsをクラスフィールドとして使用
Structはクラスフィールドの型として使用することができます。その場合、クラス内のデータはパッキングされ、順序付けられたデータとして表されます(それ以外の場合、structには特別なアラインメント要件はありません)。これは特にクラス内のインデックス付きフィールドに便利です。例えば、DescriptorArrayは3つの値を持つstructの配列を含んでいます:
struct DescriptorEntry {
key: Name|Undefined;
details: Smi|Undefined;
value: JSAny|Weak<Map>|AccessorInfo|AccessorPair|ClassPositions;
}
extern class DescriptorArray extends HeapObject {
const number_of_all_descriptors: uint16;
number_of_descriptors: uint16;
raw_number_of_marked_descriptors: uint16;
filler16_bits: uint16;
enum_cache: EnumCache;
descriptors[number_of_all_descriptors]: DescriptorEntry;
}
参照とスライス
Reference<T>およびSlice<T>は、ヒープオブジェクト内のデータへのポインタを表す特殊なstructです。両方ともオブジェクトとオフセットを含みます。Slice<T>はさらに長さを含みます。これらのstructを直接構築する代わりに、特別な構文を使用することができます:&o.xはオブジェクトo内のフィールドxへのReferenceを作成し、xがインデックス付きフィールドである場合はデータへのSliceを作成します。参照とスライスの両方について、定数版と可変版があります。参照については、これらのタイプは&Tおよびconst &Tとして、可変および定数の参照をそれぞれ表します。可変性はそれらが指すデータを指しますが、必ずしもグローバルには保持されない可能性があります。つまり、可変データへの定数参照を作成することができます。スライスの場合、タイプに特別な構文はなく、2つのバージョンはConstSlice<T>およびMutableSlice<T>として書かれます。参照はC++と同様に*または->でデリファレンスできます。
タグ付けされていないデータへの参照およびスライスは、オフヒープデータを指すこともできます。
ビットフィールド構造体
bitfield structは、単一の数値にパックされた数値データのコレクションを表します。構文は通常のstructと似ていますが、各フィールドにためのビット数が追加されます。
bitfield struct DebuggerHints extends uint31 {
side_effect_state: int32: 2 bit;
debug_is_blackboxed: bool: 1 bit;
computed_debug_is_blackboxed: bool: 1 bit;
debugging_id: int32: 20 bit;
}
ビットフィールド構造体(または他の数値データ)がSmi内に保存されている場合、SmiTagged<T>型を使用して表すことができます。
関数ポインタの型
関数ポインタは、Torqueで定義された組み込み関数のみを指すことができます。これはデフォルトのABIを保証するためです。特にバイナリコードサイズを削減するために有用です。
関数ポインタの型は匿名(Cのように)ですが、型エイリアスにバインドすることができます(Cにおけるtypedefのように)。
type CompareBuiltinFn = builtin(implicit context: Context)(Object, Object, Object) => Number;
特殊な型
voidとneverというキーワードで示される2つの特殊な型があります。voidは値を返さない呼び出し可能なものの戻り型として使用され、neverは実際には戻らず(つまり例外的な経路でのみ終了する)呼び出し可能なものの戻り型として使用されます。
一時的な型
V8ではヒープオブジェクトはランタイムでレイアウトを変更することがあります。変更の対象となるオブジェクトレイアウトや、型システムにおける他の一時的な仮定を表現するために、Torqueは「一時的な型」という概念をサポートしています。抽象型を宣言する際に、キーワードtransientを追加することで、その型を一時的な型としてマークします。
// JSArrayマップを持つHeapObjectで、グローバルNoElementsProtectorが無効化されていない場合に
// 高速にパックされた要素または高速に穴のある要素を持つ。
transient type FastJSArray extends JSArray
generates 'TNode<JSArray>';
例えば、FastJSArrayの場合、配列が辞書形式の要素に変更されたり、グローバルNoElementsProtectorが無効化された場合、一時的な型は無効化されます。これをTorqueで表現するには、潜在的にそれを行う可能性のある呼び出し可能なすべてにtransitioningと注釈を付けます。例えば、JavaScript関数を呼び出すことで任意のJavaScriptを実行できるため、それはtransitioningです。
extern transitioning macro Call(implicit context: Context)
(Callable, Object): Object;
これが型システムで規制される方法は、変換操作を超えて一時的な型の値にアクセスすることが違法であるということです。
const fastArray : FastJSArray = Cast<FastJSArray>(array) otherwise Bailout;
Call(f, Undefined);
return fastArray; // 型エラー: fastArrayはここで無効です。
Enums
列挙型は、一連の定数を定義し、それらをC++のenumクラスに類似した名前の下にグループ化する手段を提供します。宣言はenumキーワードによって導入され、次の構文構造に従います。
EnumDeclaration :
extern enum IdentifierName ExtendsDeclaration opt ConstexprDeclaration opt { IdentifierName list+ (, ...) opt }
基本的な例は以下のようになります。
extern enum LanguageMode extends Smi {
kStrict,
kSloppy
}
この宣言は新しい型LanguageModeを定義し、extends節は基礎となる型、つまり列挙型の値を表すために使用されるランタイム型を指定します。この例では、Smi型が生成するTNode<Smi>です。constexpr LanguageModeは、生成されたCSAファイル内で列挙型のデフォルト名を置き換えるconstexpr節が指定されていないため、LanguageModeに変換されます。extends節が省略されると、Torqueは型のconstexprバージョンのみを生成します。externキーワードは、この列挙型がC++で定義されていることをTorqueに伝えます。現在、extern列挙型のみがサポートされています。
Torqueは列挙型の各エントリに対して独自の型と定数を生成します。それらは列挙型の名前に一致する名前空間内に定義されます。FromConstexpr<>の必要な特殊化は、エントリのconstexpr型を列挙型に変換するために生成されます。C++ファイル内でエントリの値は<enum-constexpr>::<entry-name>という形式で生成され、ここで<enum-constexpr>は列挙型のために生成されたconstexpr名です。上記の例では、これらはLanguageMode::kStrictとLanguageMode::kSloppyです。
Torqueの列挙型はtypeswitch構文と非常によく連携します。なぜなら、値が独自の型を使用して定義されるからです。
typeswitch(language_mode) {
case (LanguageMode::kStrict): {
// ...
}
case (LanguageMode::kSloppy): {
// ...
}
}
もし列挙型が.tqファイルで使用される値よりも多くの値を持つC++定義を含んでいる場合、Torqueはそのことを知る必要があります。これは、最後のエントリの後に...を追加して列挙型を「開いた」と宣言することで行われます。例えば、一部のオプションのみがTorque内で使用可能となるExtractFixedArrayFlagを考えてみましょう。
enum ExtractFixedArrayFlag constexpr 'CodeStubAssembler::ExtractFixedArrayFlag' {
kFixedDoubleArrays,
kAllFixedArrays,
kFixedArrays,
...
}
Callables
CallableはJavaScriptやC++の関数に概念的には似ていますが、CSAコードやV8ランタイムと有用に相互作用できる追加のセマンティクスを持っています。Torqueは複数の種類のCallableを提供します:macro、builtin、runtime、およびintrinsic。
CallableDeclaration :
MacroDeclaration
BuiltinDeclaration
RuntimeDeclaration
IntrinsicDeclaration
macro Callable
マクロは生成されたCSA生成C++のチャンクに対応するCallableです。macroはTorqueで完全に定義される場合もあり、その場合CSAコードはTorqueによって生成されます。または、externでマークされる場合があり、その場合は手書きのCSAコードをCodeStubAssemblerクラスで提供する必要があります。概念的には、呼び出し箇所でインライン化されるCSAコードのチャンクとしてmacroを考えると役立ちます。
macro宣言はTorqueで次の形式を取ります。
MacroDeclaration :
transitioning opt macro IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt LabelsDeclaration opt StatementBlock
extern transitioning opt macro IdentifierName ImplicitParameters opt ExplicitTypes ReturnType opt LabelsDeclaration opt ;
すべての非extern Torque macroは、そのStatementBlock本体を使用して、その名前空間の生成されたAssemblerクラスのCSA生成機能を作成します。このコードは、code-stub-assembler.ccにある他のコードと非常によく似ていますが、マシン生成されたためにやや読みにくいかもしれません。externでマークされたmacroはTorqueには本体が書かれておらず、手書きのC++ CSAコードにインターフェイスを提供し、それをTorqueから利用できるようにします。
macro定義は暗黙的および明示的なパラメータ、オプションの返り値型、およびオプションのラベルを指定します。パラメータと返り値型は以下で詳細に説明されますが、現時点ではこれらがTypeScriptのパラメータのように動作することを知っておくと十分です。それについては、TypeScriptドキュメントの関数型セクションこちらに記載されています。
ラベルは、macroからの例外的な終了のためのメカニズムです。それらはCSAラベルに1:1で対応し、macroから生成されたC++メソッドのCodeStubAssemblerLabels*型のパラメータとして追加されます。その正確な意味については後述しますが、macroの宣言においては、カンマで区切られたラベルのリストがlabelsキーワードでオプションとして提供され、macroのパラメータリストと戻り値の型の後に配置されます。
以下は、base.tqからの外部およびTorqueで定義されたmacroの例です:
extern macro BranchIfFastJSArrayForCopy(Object, Context): never
labels Taken, NotTaken;
macro BranchIfNotFastJSArrayForCopy(implicit context: Context)(o: Object):
never
labels Taken, NotTaken {
BranchIfFastJSArrayForCopy(o, context) otherwise NotTaken, Taken;
}
builtin呼び出し可能
builtinは、完全にTorqueで定義されるか、またはexternとしてマークされる点でmacroに似ています。Torqueベースのbuiltinの場合、builtinの本文はV8 builtinを生成するために使用され、これは他のV8のbuiltinと同様に呼び出すことができ、builtin-definitions.hに関連情報を自動的に追加します。macroと同様に、Torqueのbuiltinがexternとしてマークされている場合、Torqueベースの本文は存在せず、既存のV8 builtinへのインターフェースのみが提供され、これによりTorqueコードから使用することが可能になります。
builtinのTorqueでの宣言形式は次の通りです:
MacroDeclaration :
transitioning opt javascript opt builtin IdentifierName ImplicitParameters opt ExplicitParametersOrVarArgs ReturnType opt StatementBlock
extern transitioning opt javascript opt builtin IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;
Torqueのbuiltinコードは1つのコピーのみ存在し、それは生成されたbuiltinコードオブジェクト内にあります。macroとは異なり、Torqueコードからbuiltinを呼び出した場合、CSAコードは呼び出し元でインライン化されるのではなく、代わりにそのbuiltinへの呼び出しが生成されます。
builtinはラベルを持つことができません。
builtinの実装をコーディングする場合、builtin内の最終的な呼び出しである場合に限り、builtinまたはランタイム関数へのtailcallを作成することができます。この場合、コンパイラは新しいスタックフレームを作成しないようにする可能性があります。単に呼び出しの前にtailを追加するだけです。例: tail MyBuiltin(foo, bar);
runtime呼び出し可能
runtimeはTorqueに対して外部の機能性へのインターフェースを提供できる点でbuiltinに似ています。しかし、CSAで実装される代わりに、runtimeが提供する機能性は常に標準的なランタイムコールバックとしてV8で実装されなければなりません。
runtimeのTorqueでの宣言形式は次の通りです:
MacroDeclaration :
extern transitioning opt runtime IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;
extern runtimeとして指定された名前IdentifierNameは、Runtime::kIdentifierNameによって指定されるランタイム関数に対応します。
builtinと同様に、runtimeはラベルを持つことができません。
適切な場合、ランタイム関数をtailcallとして呼び出すことも可能です。単に呼び出しの前にtailキーワードを含めるだけです。
ランタイム関数宣言は、しばしばruntimeと呼ばれる名前空間内に配置されます。この名前空間は同じ名前のbuiltinと区別し、呼び出し元でランタイム関数を呼び出していることをより明確にします。この命名規則を必須にすることを検討すべきです。
intrinsic呼び出し可能
intrinsicはTorqueで提供される内部機能性にアクセスするためのbuiltin呼び出し可能です。それらはTorqueで宣言されるものの、実装はTorqueコンパイラが提供するため定義されません。intrinsicの宣言形式は次の文法を使用します:
IntrinsicDeclaration :
intrinsic % IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt ;
ほとんどの場合、“ユーザー”Torqueコードがintrinsicを直接使用する必要はほとんどありません。
以下はサポートされているintrinsicの例です:
// %RawObjectCastは、オブジェクトを目的の型であるかどうかを厳密にテストせずに、オブジェクトからそのサブタイプにキャストします。
// RawObjectCastはTorqueコードのどこでも(ほとんどの場合は)*絶対に*使用されるべきではありませんが、
// 適切なassert()による型アサーションの後にTorqueベースのUnsafeCast演算子で使用されます。
intrinsic %RawObjectCast<A: type>(o: Object): A;
// %RawPointerCastは、RawPtrを目的の型であるかどうかを厳密にテストせずに、そのサブタイプにキャストします。
intrinsic %RawPointerCast<A: type>(p: RawPtr): A;
// %RawConstexprCastは、コンパイル時定数値を別の値に変換します。
// ソース型と目的型の両方は'constexpr'でなければなりません。
// %RawConstexprCastは生成されたC++コード内でstatic_castに変換されます。
intrinsic %RawConstexprCast<To: type, From: type>(f: From): To;
// %FromConstexprは、constexpr値を非constexpr値に変換します。現在のところ、次の非constexpr型への変換のみがサポートされています: Smi, Number, String, uintptr, intptr, int32
intrinsic %FromConstexpr<To: type, From: type>(b: From): To;
// %Allocateは、V8のGCヒープから'size'分の未初期化オブジェクトを割り当て、その結果得られるオブジェクトポインタを
// 指定のTorqueクラスにより、コンストラクタがその後標準フィールドアクセスオペレーターを使用してオブジェクトを初期化できるようにします。
// このintrinsicはTorqueコードから呼び出されるべきではありません。それは、'new'演算子を脱糖化するときに内部的に使用されます。
// 内部的に使用されるものでTorqueコードから呼び出されるべきではありません。
// 内部的に使用されます。
intrinsic %Allocate<Class: type>(size: intptr): Class;
builtinやruntimeと同様に、intrinsicはラベルを持つことはできません。
明示的なパラメーター
Torqueで定義されたCallable(Torqueのmacroやbuiltinなど)は、明示的なパラメータリストを持ちます。これらのリストは型付きTypeScript関数のパラメータリストを思わせる構文を使用しますが、Torqueではオプションのパラメータやデフォルトのパラメータをサポートしていません。さらに、Torqueで実装されたbuiltinは、V8の内部JavaScript呼び出し規約を使用するbuiltin(例: javascriptキーワードが付いているもの)である場合、任意にrestパラメータをサポートすることができます。
ExplicitParameters :
( ( IdentifierName : TypeIdentifierName ) list* )
( ( IdentifierName : TypeIdentifierName ) list+ (, ... IdentifierName ) opt )
例としては次のようなものがあります。
javascript builtin ArraySlice(
(implicit context: Context)(receiver: Object, ...arguments): Object {
// …
}
暗黙的なパラメーター
TorqueのCallableは、Scalaの暗黙的パラメーターに似たものを使用して暗黙的なパラメーターを指定することができます。
ImplicitParameters :
( implicit ( IdentifierName : TypeIdentifierName ) list* )
具体的には、macroは明示的なパラメーターに加えて暗黙的なパラメーターを宣言することができます。
macro Foo(implicit context: Context)(x: Smi, y: Smi)
CSAにマッピングする際、暗黙的パラメーターと明示的パラメーターは同じように処理され、共同のパラメーターリストを形成します。
暗黙的パラメーターは呼び出しの際に言及されず、暗黙的に渡されます: Foo(4, 5)。これが機能するためには、Foo(4, 5) はcontextという名前の値を提供するコンテキストで呼び出される必要があります。例:
macro Bar(implicit context: Context)() {
Foo(4, 5);
}
Scalaとは対照的に、暗黙的パラメーターの名前が同一でない場合はこれを禁止します。
オーバーロード解決が混乱を招く可能性があるため、暗黙的パラメーターがオーバーロード解決に全く影響を与えないことを保証しています。すなわち、オーバーロードセットの候補を比較する際、呼び出しサイトで利用可能な暗黙的なバインディングを考慮しません。単一の最適なオーバーロードが見つかった後に、暗黙的パラメーターの暗黙的バインディングが利用可能かどうかを確認します。
暗黙的パラメーターを明示的パラメーターの左側に置くことはScalaとは異なりますが、CSAでcontextパラメーターを最初に置く既存の規約によりよく適合しています。
js-implicit
Torqueで定義されたJavaScriptリンク付きのbuiltinでは、implicitの代わりにキーワードjs-implicitを使用する必要があります。引数は次の呼び出し規約の4つのコンポーネントに限定されます。
- context:
NativeContext - receiver:
JSAny(JavaScriptでのthis) - target:
JSFunction(JavaScriptでのarguments.callee) - newTarget:
JSAny(JavaScriptでのnew.target)
すべてを宣言する必要はなく、使用したいものだけを宣言してください。例として、Array.prototype.shiftのコードはこちらです。
// https://tc39.es/ecma262/#sec-array.prototype.shift
transitioning javascript builtin ArrayPrototypeShift(
js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
...
context引数がNativeContextである点に注意してください。これは、V8のbuiltinが常にそのクロージャの中にネイティブコンテキストを埋め込むためです。このjs-implicit規約でこれをエンコードすることで、関数コンテキストからネイティブコンテキストをロードする操作を排除することができます。
オーバーロード解決
Torqueのmacroや演算子(macroの別名)は、引数の型をオーバーロードすることができます。オーバーロード規則はC++の規則からインスパイアされています: オーバーロードは他のすべての代替よりも厳密に優れている場合に選択されます。これは、少なくとも1つのパラメーターで厳密に優れていて、他のすべてのパラメーターでより良いか等しく良い必要があることを意味します。
2つのオーバーロードの対応するパラメーターのペアを比較する場合、
- 以下の場合、それらは等しく良いと見なされます:
- それらが等しい場合;
- 両方が何らかの暗黙的変換を必要とする場合。
- 以下の場合、一方が他方より優れていると見なされます:
- 一方が他方の厳密なサブタイプである場合;
- 一方が暗黙的な変換を必要とせず、他方が必要とする場合。
すべての代替よりも厳密に優れているオーバーロードがない場合、これはコンパイルエラーを引き起こします。
Deferredブロック
ステートメントブロックは任意でdeferredとしてマークすることができ、これはそのブロックが比較的まれに実行されることをコンパイラに示すシグナルとなります。コンパイラはこれらのブロックを関数の末尾に配置することを選択し、非遅延コード領域のキャッシュ局所性を向上させる場合があります。例えば、Array.prototype.forEachの実装コードでは、「高速」パスに留まるよう期待され、バイアウトケースはごく稀にしか取られません:
let k: Number = 0;
try {
return FastArrayForEach(o, len, callbackfn, thisArg)
otherwise Bailout;
}
label Bailout(kValue: Smi) deferred {
k = kValue;
}
別の例として、辞書形式の要素ケースが遅延とマークされることで、より可能性の高いケースのコード生成を改善する例を示します(Array.prototype.joinの実装コードより):
if (IsElementsKindLessThanOrEqual(kind, HOLEY_ELEMENTS)) {
loadFn = LoadJoinElement<FastSmiOrObjectElements>;
} else if (IsElementsKindLessThanOrEqual(kind, HOLEY_DOUBLE_ELEMENTS)) {
loadFn = LoadJoinElement<FastDoubleElements>;
} else if (kind == DICTIONARY_ELEMENTS)
deferred {
const dict: NumberDictionary =
UnsafeCast<NumberDictionary>(array.elements);
const nofElements: Smi = GetNumberDictionaryNumberOfElements(dict);
// <etc>...
CSAコードをTorqueに移植する
Array.ofを移植したパッチは、CSAコードをTorqueに移植する際の最低限の例として役立ちます。