V8 Torque ユーザーマニュアル
V8 Torque は、V8 プロジェクトに貢献する開発者が、VM への変更の実装詳細にとらわれることなく、変更の *意図* に焦点を当てて VM の変更を表現できるようにする言語です。この言語は、ECMAScript 仕様を直接 V8 で実装できるように十分にシンプルでありながら、特定のオブジェクト形状のテストに基づいた高速パスの作成など、低レベルの V8 最適化トリックを堅牢に表現できるほど強力になるように設計されました。
Torque は、V8 エンジニアや JavaScript 開発者にとって親しみやすく、V8 コードの記述と理解を容易にする TypeScript のような構文と、すでに CodeStubAssembler
で一般的な概念を反映した構文と型を組み合わせています。強力な型システムと構造化された制御フローにより、Torque は構築による正確性を保証します。Torque の表現力は、現在 V8 の組み込み関数にある機能のほぼすべてを表現するのに十分です。また、CodeStubAssembler
の組み込み関数や C++ で記述された macro
との相互運用性も非常に高く、Torque コードで手書きの CSA 機能を使用したり、その逆も可能です。
Torque は、V8 実装の高度で意味的に豊かな断片を表現するための言語構造を提供し、Torque コンパイラはこれらの断片を CodeStubAssembler
を使用して効率的なアセンブリコードに変換します。Torque の言語構造と Torque コンパイラの誤りチェックの両方が、以前は CodeStubAssembler
を直接使用することで手間がかかり、エラーが発生しやすかった方法で正確性を保証します。従来、CodeStubAssembler
で最適なコードを作成するには、V8 エンジニアが多くの専門知識を頭の中に持っている必要がありましたが、その多くは書面によるドキュメントに正式に記録されていませんでした。その知識がなければ、効率的な組み込み関数を作成するための学習曲線は急勾配でした。必要な知識を身に着けていても、明白ではなく監視されていない落とし穴が、しばしば正確性や セキュリティ バグにつながりました。Torque を使用すると、これらの落とし穴の多くを回避し、Torque コンパイラによって自動的に認識できます。
はじめに #
Torque で記述されたほとんどのソースは、src/builtins
ディレクトリの V8 リポジトリに .tq
ファイル拡張子でチェックインされています。V8 のヒープ割り当てクラスの Torque 定義は、src/objects
内の対応する C++ ファイルと同じ名前の .tq
ファイル内で、C++ 定義とともに見つけることができます。実際の Torque コンパイラは、src/torque
にあります。Torque 機能のテストは、test/torque
、test/cctest/torque
、および test/unittests/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, 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
は、次のファイルを生成します- 生成された CSA マクロを含む
path/to/file-tq-csa.cc
およびpath/to/file-tq-csa.h
。 - クラス定義を含む対応するヘッダー
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
ファイルを生成します。- 生成された CSA マクロを含む
次に、
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
定義と、それらのextern
定義が参照する C++ コードとの間の結合は緩く、Torque コンパイル時にその結合の検証はありません。extern
定義がcode-stub-assembler.h
ヘッダーファイルまたはその他の V8 ヘッダーでアクセスする機能と一致しない場合(または最も微妙なケースではマスクする場合)、mksnapshot
の C++ ビルドは失敗します。 mksnapshot
が正常にビルドされた後でも、実行中に失敗する可能性があります。これは、たとえば、Torque のstatic_assert
を Turbofan で検証できないため、Turbofan が生成された CSA コードのコンパイルに失敗する場合に発生する可能性があります。また、スナップショット作成中に実行される Torque 提供の組み込み関数にバグがある可能性もあります。たとえば、Torque で作成された組み込み関数であるArray.prototype.splice
は、デフォルトの JavaScript 環境を設定するために JavaScript スナップショット初期化プロセスの一部として呼び出されます。実装にバグがある場合、mksnapshot
は実行中にクラッシュします。mksnapshot
がクラッシュした場合、mksnapshot
に--gdb-jit-full
フラグを渡して呼び出すと便利な場合があります。これにより、gdb
スタックトレースで Torque 生成の組み込み関数の名前など、有用なコンテキストを提供する追加のデバッグ情報が生成されます。- もちろん、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 コンパイラで完全に評価することはできませんが、代わりに、mksnapshot
を実行するときに完全に評価できる(および評価する必要がある)C++ 型、変数、および式にマップされます。Torque ライターの視点から見ると、constexpr
式はランタイム時に実行されるコードを生成しないため、その意味ではコンパイル時になります。これは、技術的には mksnapshot
が実行する Torque の外部にある C++ コードによって評価されているとしても同様です。したがって、Torque では、constexpr
は基本的に「コンパイル時」ではなく「mksnapshot
時」を意味します。
ジェネリックと組み合わせて、constexpr
は、V8 開発者が事前に予測できる特定の詳細がわずかに異なる、複数の非常に効率的な特殊化された組み込み関数の生成を自動化するために使用できる強力な Torque ツールです。
ファイル #
Torqueコードは、個々のソースファイルにパッケージ化されています。各ソースファイルは一連の宣言で構成されており、これらの宣言は名前空間を分離するために、名前空間宣言でオプションでラップできます。以下の文法の説明は、おそらく古くなっています。真実はTorqueコンパイラの文法定義にあり、これは文脈自由文法ルールを用いて記述されています。
Torqueファイルは、宣言のシーケンスです。可能な宣言は、torque-parser.cc
にリストされています。
名前空間 #
Torqueの名前空間により、宣言を独立した名前空間に含めることができます。これらはC++の名前空間に似ています。他の名前空間では自動的に表示されない宣言を作成できます。ネストすることができ、ネストされた名前空間内の宣言は、修飾なしにそれらを含む名前空間の宣言にアクセスできます。明示的に名前空間宣言に含まれていない宣言は、すべての名前空間から見える共有のグローバルデフォルト名前空間に配置されます。名前空間は再度開くことができ、複数のファイルにわたって定義できます。
例:
macro IsJSObject(o: Object): bool { … } // In default namespace
namespace array {
macro IsJSArray(o: Object): bool { … } // In array namespace
};
namespace string {
// …
macro TestVisibility() {
IsJsObject(o); // OK, global namespace visible here
IsJSArray(o); // ERROR, not visible in this namespace
array::IsJSArray(o); // OK, explicit namespace qualification
}
// …
};
namespace array {
// OK, namespace has been re-opened.
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型のconstexpr
バージョンに対応するC++型を指定する文字列リテラルです。
以下は、Torqueの31ビットおよび32ビットの符号付き整数型に対するbase.tq
からの例です。
type int32 generates 'TNode<Int32T>' constexpr 'int32_t';
type int31 extends int32 generates 'TNode<Int32T>' constexpr 'int31_t';
共用体型 #
共用体型は、値が複数の可能な型の1つに属することを表します。タグ付けされた値に対してのみ共用体型を許可します。これは、マップポインタを使用して実行時に区別できるためです。たとえば、JavaScriptの数値は、Smi値または割り当てられたHeapNumber
オブジェクトのいずれかです。
type Number = Smi | HeapNumber;
共用体型は、次の等式を満たします
A | B = B | A
A | (B | C) = (A | B) | C
B
がA
のサブタイプの場合、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> {
// Whatever the class needs beyond Torque-generated stuff goes here...
// At the end, because it messes with public/private:
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)
// Rest of class omitted...
}
@generateBodyDescriptor
は、ガベージコレクターがオブジェクトをどのように訪問する必要があるかを表す、生成されたクラス内にクラスBodyDescriptor
をTorqueに生成させます。それ以外の場合、C++コードは独自のオブジェクト訪問を定義するか、既存のパターンの1つを使用する必要があります(たとえば、Struct
から継承し、STRUCT_LIST
にクラスを含めることは、クラスにタグ付きの値のみが含まれていると予想されることを意味します)。
@generatePrint
アノテーションを追加すると、ジェネレーターはTorqueレイアウトで定義されたフィールド値を印刷するC++関数を実装します。JSProxyの例を使用すると、シグネチャはvoid TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyPrint(std::ostream& os)
になり、これはJSProxy
によって継承できます。
Torqueコンパイラは、クラスが@noVerifier
アノテーションでオプトアウトしない限り、すべてのextern
クラスに対して検証コードも生成します。たとえば、上記のJSProxyクラス定義は、Torque型定義に従ってそのフィールドが有効であることを検証するC++メソッドvoid TorqueGeneratedClassVerifiers::JSProxyVerify(JSProxy o, Isolate* isolate)
を生成します。また、生成されたクラスに、TorqueGeneratedClassVerifiers
から静的関数を呼び出す対応する関数TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyVerify
を生成します。クラスの追加検証(数値の許容値範囲、またはフィールドfoo
がフィールドbar
が非nullの場合にtrueである必要があるなどの要件など)を追加する場合は、C++クラスにDECL_VERIFIER(JSProxy)
を追加し(継承されたJSProxyVerify
を隠します)、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がJSヒープ内のオブジェクトの型を実行時に決定できるように、すべてのクラスに対してv8::internal::InstanceType
列挙型で一意の値を割り当てる役割を担っています。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
は、一部のカスタムBodyDescriptor
で使用されているレガシー機能であるkEndOfStrongFieldsOffset
やkStartOfWeakFieldsOffset
などの定数の生成に影響を与え、現在でも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` の代わりにキーワード `shape` を使用することを除いて、`class` を定義することとよく似ています。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;
}
構造体のアノテーション #
@export
としてマークされた構造体は、生成されたファイル `gen/torque-generated/csa-types.h` に予測可能な名前で含まれます。名前の先頭には `TorqueStruct` が付加されるため、`PromiseResolvingFunctions` は `TorqueStructPromiseResolvingFunctions` になります。
構造体のフィールドは `const` としてマークでき、これは書き込みを行わないことを意味します。構造体全体は上書きできます。
クラスフィールドとしての構造体 #
構造体はクラスフィールドの型として使用できます。その場合、クラス内のパックされた順序付きデータを表します(それ以外の場合、構造体にはアラインメント要件はありません)。これは、クラスのインデックス付きフィールドに特に役立ちます。例として、`DescriptorArray` には3つの値を持つ構造体の配列が含まれています。
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>
は、ヒープオブジェクト内に保持されているデータへのポインタを表す特別な構造体です。どちらもオブジェクトとオフセットを含みます。Slice<T>
には長さも含まれます。これらの構造体を直接構築するのではなく、特別な構文を使用できます。&o.x
は、オブジェクト `o` 内のフィールド `x` への `Reference` を作成するか、`x` がインデックス付きフィールドの場合はデータへの `Slice` を作成します。参照とスライスの両方に、const バージョンと可変バージョンがあります。参照の場合、これらの型は可変参照と定数参照に対してそれぞれ `&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>` を使用して表すことができます。
関数ポインタ型 #
関数ポインタは、デフォルトの ABI を保証するため、Torque で定義された組み込み関数のみを指すことができます。これらは、バイナリコードサイズを削減するのに特に役立ちます。
関数ポインタ型は(C のように)匿名ですが、(C の `typedef` のように)型エイリアスにバインドできます。
type CompareBuiltinFn = builtin(implicit context: Context)(Object, Object, Object) => Number;
特別な型 #
キーワード `void` と `never` で示される2つの特別な型があります。`void` は値を返さない呼び出し可能オブジェクトの戻り値の型として使用され、`never` は実際には戻らない(つまり、例外パスのみを介して終了する)呼び出し可能オブジェクトの戻り値の型として使用されます。
一時的な型 #
V8 では、ヒープオブジェクトは実行時にレイアウトを変更できます。変更される可能性のあるオブジェクトレイアウトや、型システムの一時的な仮定を表すために、Torque は「一時的な型」の概念をサポートしています。抽象型を宣言するとき、キーワード `transient` を追加すると、一時的な型としてマークされます。
// A HeapObject with a JSArray map, and either fast packed elements, or fast
// holey elements when the global NoElementsProtector is not invalidated.
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; // Type error: fastArray is invalid here.
列挙型 #
列挙型は、C++ の列挙型クラスと同様に、一連の定数を定義し、名前でグループ化する手段を提供します。
宣言は `enum` キーワードによって導入され、以下に従います。
構文構造
EnumDeclaration :
extern enum IdentifierName ExtendsDeclaration opt ConstexprDeclaration opt { IdentifierName list+ (, ...) opt }
基本的な例は次のようになります。
extern enum LanguageMode extends Smi {
kStrict,
kSloppy
}
この宣言は、新しい型 `LanguageMode` を定義し、`extends` 句は基になる
型、つまり列挙型の値を表すために使用されるランタイム型を指定します。この例では、これは `TNode<Smi>` です。
これは型 `Smi` が `生成` するものです。`constexpr LanguageMode` は `LanguageMode` に変換されます。
生成された CSA ファイルでは、デフォルト名を置き換えるための `constexpr` 句が列挙型に指定されていないためです。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): {
// ...
}
}
列挙型の C++ 定義に、`.tq` ファイルで使用されている値よりも多くの値が含まれている場合、Torque はそれを知る必要があります。これは、最後のエントリの後に `...` を追加して、列挙型を「開いている」と宣言することで行われます。たとえば、`ExtractFixedArrayFlag` を検討してください。ここでは、オプションの一部のみが
Torque 内から利用可能/使用されます。
enum ExtractFixedArrayFlag constexpr 'CodeStubAssembler::ExtractFixedArrayFlag' {
kFixedDoubleArrays,
kAllFixedArrays,
kFixedArrays,
...
}
呼び出し可能オブジェクト #
呼び出し可能オブジェクトは概念的には JavaScript または C++ の関数に似ていますが、CSA コードや V8 ランタイムと便利な方法で対話できるようにする追加のセマンティクスがいくつかあります。Torque は、`macro`、`builtin`、`runtime`、`intrinsic` のいくつかの異なるタイプの呼び出し可能オブジェクトを提供します。
CallableDeclaration :
MacroDeclaration
BuiltinDeclaration
RuntimeDeclaration
IntrinsicDeclaration
macro
呼び出し可能オブジェクト #
マクロは、生成された CSA を生成する C++ のチャンクに対応する呼び出し可能オブジェクトです。`macro` は、Torque で完全に定義することもできます。その場合、CSA コードは Torque によって生成されるか、`extern` とマークされます。その場合、実装は CodeStubAssembler クラスで手書きの CSA コードとして提供する必要があります。概念的には、`macro` をコールサイトでインライン化されるインライン可能な CSA コードのチャンクと考えると便利です。
Torque での macro
宣言は、次の形式を取ります。
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` は、`macro` の `StatementBlock` 本体を使用して、名前空間の生成された `Assembler` クラスに CSA 生成関数を作成します。このコードは、`code-stub-assembler.cc` にある他のコードと非常によく似ていますが、機械生成されているため、少し読みにくいです。`extern` とマークされた `macro` には Torque で記述された本体がなく、Torque から使用できるように、手書きの C++ CSA コードへのインターフェイスを提供するだけです。
macro
の定義は、暗黙的および明示的なパラメータ、オプションの戻り値の型、オプションのラベルを指定します。パラメータと戻り値の型については後で詳しく説明しますが、今のところ、TypeScript パラメータのように動作することを知っていれば十分です。TypeScript ドキュメントの関数型セクションで説明されているように、ここ。
ラベルは、`macro` からの例外的な終了のためのメカニズムです。これらは CSA ラベルに 1:1 でマッピングされ、`macro` 用に生成された C++ メソッドに `CodeStubAssemblerLabels*` 型のパラメータとして追加されます。それらの正確なセマンティクスについては後で説明しますが、`macro` の宣言の目的のために、`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-definitions.h` に関連情報を自動的に追加するなど、他の V8 組み込み関数と同様に呼び出すことができる V8 組み込み関数を生成します。`macro` と同様に、`extern` とマークされた Torque `builtin` には Torque ベースの本体はなく、Torque コードから使用できるように、既存の V8 `builtin` へのインターフェイスを提供するだけです。
Torqueにおけるbuiltin
宣言は以下の形式を持ちます。
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に対するコードは、生成されたbuiltinコードオブジェクト内に1つだけ存在します。macro
とは異なり、Torqueコードからbuiltin
が呼び出される際、CSAコードは呼び出し箇所にインライン展開されず、代わりにbuiltinへの呼び出しが生成されます。
builtin
はラベルを持つことができません。
builtin
の実装をコーディングする場合、builtinまたはランタイム関数への末尾呼び出しを行うことができます。ただし、builtinの最後の呼び出しである場合に限ります。この場合、コンパイラは新しいスタックフレームの作成を回避できる可能性があります。呼び出しの前にtail
を追加するだけで、例えば tail MyBuiltin(foo, bar);
のように記述します。
runtime
呼び出し可能オブジェクト #
runtime
は、外部機能へのインターフェースをTorqueに公開できるという点でbuiltin
に似ています。ただし、CSAで実装される代わりに、runtime
によって提供される機能は常にV8で標準ランタイムコールバックとして実装される必要があります。
Torqueにおけるruntime
宣言は以下の形式を持ちます。
MacroDeclaration :
extern transitioning opt runtime IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;
名前IdentifierNameで指定されたextern runtime
は、Runtime::kIdentifierName
で指定されたランタイム関数に対応します。
builtin
と同様に、runtime
はラベルを持つことができません。
適切な場合は、runtime
関数を末尾呼び出しとして呼び出すこともできます。呼び出しの前にtail
キーワードを含めるだけです。
ランタイム関数の宣言は、しばしばruntime
という名前空間に配置されます。これにより、同じ名前のビルトインとの区別がつき、呼び出し箇所でランタイム関数を呼び出していることが容易にわかります。これを必須にすることを検討すべきです。
intrinsic
呼び出し可能オブジェクト #
intrinsic
は、Torqueで実装できない内部機能へのアクセスを提供する組み込みのTorque呼び出し可能オブジェクトです。これらはTorqueで宣言されますが、実装はTorqueコンパイラによって提供されるため定義されません。intrinsic
宣言は次の文法を使用します。
IntrinsicDeclaration :
intrinsic % IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt ;
ほとんどの場合、「ユーザー」Torqueコードがintrinsic
を直接使用する必要はほとんどありません。
以下は、サポートされている組み込み関数の一部です。
// %RawObjectCast downcasts from Object to a subtype of Object without
// rigorous testing if the object is actually the destination type.
// RawObjectCasts should *never* (well, almost never) be used anywhere in
// Torque code except for in Torque-based UnsafeCast operators preceeded by an
// appropriate type assert()
intrinsic %RawObjectCast<A: type>(o: Object): A;
// %RawPointerCast downcasts from RawPtr to a subtype of RawPtr without
// rigorous testing if the object is actually the destination type.
intrinsic %RawPointerCast<A: type>(p: RawPtr): A;
// %RawConstexprCast converts one compile-time constant value to another.
// Both the source and destination types should be 'constexpr'.
// %RawConstexprCast translate to static_casts in the generated C++ code.
intrinsic %RawConstexprCast<To: type, From: type>(f: From): To;
// %FromConstexpr converts a constexpr value into into a non-constexpr
// value. Currently, only conversion to the following non-constexpr types
// are supported: Smi, Number, String, uintptr, intptr, and int32
intrinsic %FromConstexpr<To: type, From: type>(b: From): To;
// %Allocate allocates an unitialized object of size 'size' from V8's
// GC heap and "reinterpret casts" the resulting object pointer to the
// specified Torque class, allowing constructors to subsequently use
// standard field access operators to initialize the object.
// This intrinsic should never be called from Torque code. It's used
// internally when desugaring the 'new' operator.
intrinsic %Allocate<Class: type>(size: intptr): Class;
builtin
やruntime
と同様に、intrinsic
もラベルを持つことはできません。
明示的なパラメータ #
Torqueで定義された呼び出し可能オブジェクト(例えばTorqueのmacro
やbuiltin
)の宣言には、明示的なパラメータリストがあります。これは、型付きTypeScript関数のパラメータリストを彷彿とさせる構文を使用した識別子と型のペアのリストですが、Torqueはオプションパラメータやデフォルトパラメータをサポートしていない点が異なります。さらに、Torque実装のbuiltin
は、組み込み関数がV8の内部JavaScript呼び出し規約を使用する場合(例えば、javascript
キーワードでマークされている場合)、オプションで可変長引数パラメータをサポートできます。
ExplicitParameters :
( ( IdentifierName : TypeIdentifierName ) list* )
( ( IdentifierName : TypeIdentifierName ) list+ (, ... IdentifierName ) opt )
例として
javascript builtin ArraySlice(
(implicit context: Context)(receiver: Object, ...arguments): Object {
// …
}
暗黙的なパラメータ #
Torqueの呼び出し可能オブジェクトは、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とは対照的に、暗黙的なパラメータの名前が同一でない場合は、これを禁止します。
オーバーロード解決は紛らわしい動作を引き起こす可能性があるため、暗黙的なパラメータがオーバーロード解決にまったく影響を与えないようにします。つまり、オーバーロードセットの候補を比較する場合、呼び出し箇所で使用可能な暗黙的なバインディングは考慮しません。最適なオーバーロードが1つ見つかった後にのみ、暗黙的なパラメータに対する暗黙的なバインディングが利用可能かどうかを確認します。
暗黙的なパラメータを明示的なパラメータの左側に配置することは、Scalaとは異なりますが、CSAでcontext
パラメータを最初に持つという既存の規約にうまく対応します。
js-implicit
#
Torqueで定義されたJavaScriptリンケージを持つ組み込み関数については、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の組み込み関数が常にネイティブコンテキストをクロージャに埋め込むためです。これをjs-implicit規約でエンコードすると、プログラマーは関数コンテキストからネイティブコンテキストをロードする操作を排除できます。
オーバーロード解決 #
Torqueのmacro
と演算子(macro
のエイリアスにすぎません)は、引数の型によるオーバーロードを許可します。オーバーロード規則はC++のものに触発されています。オーバーロードは、すべての代替案よりも厳密に優れている場合に選択されます。つまり、少なくとも1つのパラメータで厳密に優れており、他のすべてのパラメータで優れているか、または同等である必要があります。
2つのオーバーロードの対応するパラメータのペアを比較する場合…
- …それらは次の場合に同等であるとみなされます。
- それらが等しい場合。
- 両方が暗黙的な変換を必要とする場合。
- …一方が優れているとみなされるのは、次の場合です。
- それが他方の厳密なサブタイプである場合。
- 一方が暗黙的な変換を必要としないが、他方はそうである場合。
すべての代替案よりも厳密に優れているオーバーロードがない場合、これはコンパイルエラーになります。
遅延ブロック #
ステートメントブロックは、オプションでdeferred
としてマークできます。これは、コンパイラに対して、そのブロックがあまり頻繁に実行されないことを示す信号です。コンパイラは、これらのブロックを関数の最後に配置して、コードの非遅延領域のキャッシュ局所性を向上させることを選択できます。たとえば、Array.prototype.forEach
の実装のこのコードでは、「高速」パスにとどまり、bailoutケースはまれにしか発生しないと予想されます。
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に移植する最小限の例となります。