V8 Torque 用户手册

V8 Torque 是一种语言,它允许参与 V8 项目的开发人员通过专注于对 VM 进行更改的 意图 来表达对 VM 的更改,而不是将精力集中于不相关的实现细节。该语言设计得足够简单,可以轻松地将 ECMAScript 规范 直接转换为 V8 中的实现,而且其有足够的能力以健壮的方式表达低级别的 V8 优化技巧,例如根据特定对象形状(object-shapes)的测试创建快速路径(fast-paths)。

Torque 对于 V8 工程师和 JavaScript 开发者来说是比较熟悉的, 因其结合了类似 TypeScript 的语法(易于编写和理解 V8 代码)和 CodeStubAssembler 中常见的语法及类型。凭借强大的类型系统和结构化的控制流程,Torque 可在结构上确保其正确性。Torque 的表现力足以让其表达 目前 V8 内置的 几乎所有的功能。它也可以与 CodeStubAssembler 内置函数和用 C++ 编写的 macro 很好地互操作,从而允许 Torque 代码使用手写的 CSA 功能,反之亦然。

Torque 提供了用语言构造来表达高级别、语义丰富的 V8 实现方式,并且 Torque 编译器使用 CodeStubAssembler 将这些文件转换为有效的汇编代码。以前直接使用 CodeStubAssembler 是费力且容易出错的,Torque 的语言结构和 Torque 编译器的错误检查确保了其正确性。传统上,使用 CodeStubAssembler编写最佳代码需要 V8 工程师掌握大量的专业知识,以避免在实现过程中埋下细微的隐患,而其中许多知识几乎不能从书面文档上获得。没有掌握这些知识,编写高效内置程序的学习曲线将非常陡峭。即使掌握了必要的知识,不是那么明显且不受管理的陷阱也经常会导致正确性问题或 安全性 漏洞。使用 Torque,这些陷阱可以通过 Torque 编译器避免和自动识别。

入门 { #getting-started }

大多数用 Torque 编写的源代码都已签入 V8 存储库下的 src/builtins 目录,文件扩展名为 .tq。 (实际的 Torque 编译器可以在 src/torque 目录下找到。)。Torque 的功能测试在 test/torque 目录下。

为了让你能体验到这种语言,我们来编写一个 V8 内置功能来打印输出 “Hello World!”。为此,我们将在测试用例中添加一个 Torque macro,并在 cctest 测试框架中调用它。

首先打开 test/torque/test-torque.tq 文件,并在末尾(在最后一个 } 之前)添加如下代码:

  1. @export
  2. macro PrintHelloWorld() {
  3. Print('Hello world!');
  4. }

接下来,打开 test/cctest/torque/test-torque.cc 文件并添加以下使用新的 Torque 代码构建的代码块(code stub)的测试用例:

  1. TEST(HelloWorld) {
  2. Isolate* isolate(CcTest::InitIsolateOnce());
  3. CodeAssemblerTester asm_tester(isolate, 0);
  4. TestTorqueAssembler m(asm_tester.state());
  5. {
  6. m.PrintHelloWorld();
  7. m.Return(m.UndefinedConstant());
  8. }
  9. FunctionTester ft(asm_tester.GenerateCode(), 0);
  10. ft.Call();
  11. }

然后 构建 cctest 可执行文件,最后执行 cctest 测试以打印 “Hello world”:

  1. $ out/x64.debug/cctest test-torque/HelloWorld
  2. Hello world!

Torque 如何生成代码 { #how-torque-generates-code }

Torque 编译器不会直接创建机器代码,而是会生成 C++ 代码,以调用 V8 现有的 CodeStubAssembler 接口。CodeStubAssembler 使用 TurboFan 编译器的 后端生成高效的代码。因此,Torque 编译需要多个步骤:

  1. gn 构建首先运行 Torque 编译器。它处理所有 *.tq 文件,在 gen/torque-generated 目录下适当的子文件夹中输出相应的 *-tq-csa.cc*-tq-csa.h 文件。Torque 编译器还会生成各种已知的 .h 文件,这些文件将由 V8 构建使用。这些包含了编译的 .tq 文件中找到的所有类定义。
  2. Torque 生成的 .h 文件包含在 V8 构建中的关键位置,补充了 V8 源文件中“手工”声明的类定义。
  3. 然后,gn 构建将步骤 1 中生成的 .cc 文件编译为 mksnapshot 可执行文件。
  4. 运行 mksnapshot 时,将生成所有 V8 内置文件并将其打包到快照文件中,包括在 Torque 中定义的那些以及使用 Torque 定义(Torque-defined)功能的任何其它内置文件。
  5. V8 的其余部分已构建。通过链接到 V8 的快照文件,可以访问所有 Torque 授权(Torque-authored)的内置文件。可以像其它任何内置方法一样调用它们。在最终包中,没有留下任何直接的 Torque 痕迹(调试信息除外):d8chrome 可执行文件中均未包含Torque源代码(.tq 文件)或 Torque 生成的 .cc 文件。

以图形方式,构建过程如下所示:

V8 Torque 用户手册 - 图1

Torque 工具 { #tooling }

Torque 提供了基本的工具和开发环境支持。

  • 有一个适用于 Torque 的 Visual Studio Code 语法突出显示插件:tools/torque/vscode-torque
  • 更改 .tq 文件后,还应该使用一种格式化工具:tools/torque/format-torque.py -i <filename>

对涉及 Torque 的构建进行故障排除 { #troubleshooting }

为什么你需要知道这一点?了解 Torque 文件如何转换为机器代码很重要,因为在将 Torque 转换为快照中嵌入的二进制位的不同阶段中可能会出现不同的问题(和错误):

  • 如果你在 Torque 代码(即 .tq 文件)中存在语法或语义错误,则 Torque 编译器将失败。 V8 构建在此阶段中止,并且你将看不到构建的后续部分可能发现的其他错误。
  • 一旦你的 Torque 代码在语法上正确无误,并通过了 Torque 编译器(或多或少)严格的语义检查,mksnapshot 的构建仍然可能失败。最常见的情况是 .tq 文件中提供的外部定义不一致。在 Torque 代码中用 extern 关键字标记的定义会向 Torque 编译器发出信号,表明所需功能的定义已在 C++ 中找到。当前,.tq 文件中的 extern 定义与这些 extern 定义所引用的 C++ 代码之间的耦合是松散的,并且在 Torque 编译时没有进行任何验证。当 extern 定义与在 code-stub-assembler.h 头文件或其它 V8 头中访问的功能不匹配(或在最微妙的情况下屏蔽)时,mksnapshot 的 C++ 构建将失败,通常在 *-gen.cc 中。
  • 即使 mksnapshot 成功构建,如果 Torque 提供的内置程序有错误,它也可能在执行期间失败。许多内置程序作为快照创建的一部分运行,包括 Torque生成的内置程序。例如,在 JavaScript 快照初始化过程中,将调用 Torque 创建的内置 Array.prototype.splice ,以设置默认的 JavaScript 环境。如果实现中存在错误,则 mksnapshot 在执行期间将会崩溃。当 mksnapshot 崩溃时,有时通过传递 --gdb-jit-full 标志来调用 mksnapshot 很有用,该标志会生成额外的调试信息,从而提供有用的上下文,例如 gdb 堆栈抓取中由 Torque 生成的内置函数的名称。
  • 当然,即使 Torque 编写的代码通过 mksnapshot 构建,它仍然可能有故障或崩溃。将测试用例添加到 torque-test.tqtorque-test.cc 是确保你的 Torque 代码达到你实际期望的一种好方法。如果你的 Torque 代码最终在 d8chrome 中崩溃,则 --gdb-jit-full 标志再次非常有用。

constexpr: 编译时与运行时 { #constexpr }

了解 Torque 构建过程对于理解 Torque 语言的核心功能 constexpr 也很重要。

Torque 允许在运行时评估 Torque 代码中的表达式(即,当 V8 内置函数作为执行 JavaScript 的一部分而执行时)。但是,它也允许在编译时执行表达式(即,作为 Torque 构建过程的一部分,并且甚至在创建 V8 库和 d8 可执行文件之前)。

Torque 使用 constexpr 关键字指示必须在构建时对表达式求值。它的用法在某种程度上类似于 C++’s constexpr:除了从 C++ 借鉴了 constexpr 关键字和它的某些语法外,Torque 同样使用 constexpr 来表示编译时和运行时评估之间的区别。

但是,Torque 的 constexpr 语义有一些细微的差异。 在 C++ 中,constexpr 表达式可以由 C++ 编译器完全求值。在 Torque 中,constexpr 表达式不能由 Torque 编译器完全评估,而是映射到 C++ 类型、变量和表达式,这些变量和表达式在运行 mksnapshot 时可以被(必须被)完全评估。从 Torque 编写者(Torque-writer)的角度来看,constexpr 表达式不会生成在运行时执行的代码,因此从某种意义上说属于编译时,但是从技术上来说,constexpr 表达式是由 mksnapshot 运行的 Torque 外部的 C++ 代码评估的。因此,在 Torque 中,constexpr 本质上是指 “mksnapshot-time”,而不是“编译时”。

与泛型结合使用时,constexpr 是一个功能强大的 Torque 工具,可用于自动生成多个非常高效的标准内置程序,这些内建函数在少量特定细节上彼此不同,而这些 V8 开发人员则可以预先预料到。

文件 { #files }

Torque 代码打包在单独的源文件中。每个源文件都包含一系列声明,它们本身可以选择包含在命名空间声明中,以分隔声明的命名空间。.tq 文件的语法如下:

  1. Declaration :
  2. AbstractTypeDeclaration
  3. ClassDeclaration
  4. TypeAliasDeclaration
  5. EnumDeclaration
  6. CallableDeclaration
  7. ConstDeclaration
  8. GenericSpecialization
  9. NamespaceDeclaration :
  10. namespace IdentifierName { Declaration* }
  11. A Torque file is a sequence of declarations. The possible declarations are listed [in `torque-parser.cc`](https://source.chromium.org/chromium/chromium/src/+/master:v8/src/torque/torque-parser.cc?q=TorqueGrammar::declaration).
  12. ## 命名空间 { #namespaces }
  13. Torque 命名空间允许声明成为独立的命名空间。它们类似于 C++ 命名空间。它们允许你创建在其它命名空间中不自动可见的声明。它们可以嵌套,并且嵌套命名空间中的声明可以无限制地访问包含它们的命名空间中的声明。未在命名空间声明中显式声明的声明被放入对所有命名空间可见的共享全局默认命名空间中。可以重新打开命名空间,从而可以在多个文件中定义它们。
  14. 例如:
  15. ```torque
  16. macro IsJSObject(o: Object): bool { … } // In default namespace
  17. namespace array {
  18. macro IsJSArray(o: Object): bool { … } // In array namespace
  19. };
  20. namespace string {
  21. // …
  22. macro TestVisibility() {
  23. IsJsObject(o); // OK, global namespace visible here
  24. IsJSArray(o); // ERROR, not visible in this namespace
  25. array::IsJSArray(o); // OK, explicit namespace qualification
  26. }
  27. // …
  28. };
  29. namespace array {
  30. // OK, namespace has been re-opened.
  31. macro EnsureWriteableFastElements(array: JSArray){ … }
  32. };

声明 { #declarations }

类型 { #types }

Torque 是强类型的。它的类型系统是它提供的许多安全性和正确性保证的基础。

但是,除了稍后讨论的几个显著例外以外,Torque 实际上并不十分了解用于编写大多数 Torque 代码的核心类型。为了使 Torque 和手写的 CodeStubAssembler 代码之间具有更好的互操作性,Torque 的类型系统严格指定了 Torque 类型之间的关系,但在指定类型本身实际工作方式方面却不那么严格。相反,它通过显式类型映射与 CodeStubAssembler 和 C++ 类型松散耦合,并且它依赖 C++ 编译器来强制执行该映射的严格操作。

在 Torque 中,有三种不同的类型:Abstract, Function 和 Union。

抽象(Abstract)类型 { #abstract-types }

Torque 的抽象(Abstract)类型直接映射到 C++ 编译时和 CodeStubAssembler 运行时值。它们的声明指定了名称和与 C++ 类型的关系:

  1. AbstractTypeDeclaration :
  2. type IdentifierName ExtendsDeclaration opt GeneratesDeclaration opt ConstexprDeclaration opt
  3. ExtendsDeclaration :
  4. extends IdentifierName ;
  5. GeneratesDeclaration :
  6. generates StringLiteral ;
  7. ConstexprDeclaration :
  8. constexpr StringLiteral ;

IdentifierName 指定抽象类型的名称,ExtendsDeclaration 可选地指定所声明的类型所源自的类型。GeneratesDeclaration 可选地指定一个字符串字面量(与 CodeStubAssembler 代码中使用的 C++ TNode 类型相对应)以包含其类型的运行时值。 ConstexprDeclaration 是一个字符串文字面量,用于指定与构建时间(mksnapshot-time)评估的 Torque 类型的 constexpr 版本相对应的 C++ 类型。

这是来自 base.tq 的示例,其中包含 Torque 的 31 位和 32 位有符号整数类型:

  1. type int32 generates 'TNode<Int32T>' constexpr 'int32_t';
  2. type int31 extends int32 generates 'TNode<Int32T>' constexpr 'int31_t';

联合(Union)类型 { #union-types }

联合(Union)类型表示值属于几种可能的类型之一。 我们仅允许标记值的联合类型,因为可以在运行时使用映射指针来区分它们。例如,JavaScript 数字是 Smi 值或已分配的 HeapNumber 对象。

  1. type Number = Smi | HeapNumber;

联合(Union)类型满足以下相等性:

  • A | B = B | A
  • A | (B | C) = (A | B) | C
  • A | B = A if B is a subtype of A

由于不允许在运行时区分未标记的类型,因此仅允许从标记的类型形成联合(Union)类型。

将联合(Union)类型映射到 CSA 时,将选择联合类型的所有类型中最具体的通用父类型,但 NumberNumeric 除外,它们映射到相应的 CSA 联合体类型。

类(Class)类型 { #class-types }

类(Class)类型使得可以通过 Torque 代码在 V8 GC 堆上定义,分配和操作结构化对象。每个 Torque 类类型必须对应于 C++ 代码中 HeapObject 的子类。 为了最大程度地减少在 V8 的 C++ 和 Torque 实现之间维护样板(boilerplate)对象访问代码的开销,Torque 类定义用于在可能的情况下(和适当时)生成所需的 C++ 对象访问代码,以减少手动保持 C++ 和 Torque 同步的麻烦。

  1. ClassDeclaration :
  2. ClassAnnotation* extern opt transient opt class IdentifierName ExtendsDeclaration opt GeneratesDeclaration opt {
  3. ClassMethodDeclaration*
  4. ClassFieldDeclaration*
  5. }
  6. ClassAnnotation :
  7. @doNotGenerateCppClass
  8. @generateBodyDescriptor
  9. @generatePrint
  10. @abstract
  11. @export
  12. @noVerifier
  13. @hasSameInstanceTypeAsParent
  14. @highestInstanceTypeWithinParentClassRange
  15. @lowestInstanceTypeWithinParentClassRange
  16. @reserveBitsInInstanceType ( NumericLiteral )
  17. @apiExposedInstanceTypeValue ( NumericLiteral )
  18. ClassMethodDeclaration :
  19. transitioning opt IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt LabelsDeclaration opt StatementBlock
  20. ClassFieldDeclaration :
  21. ClassFieldAnnotation* weak opt const opt FieldDeclaration;
  22. ClassFieldAnnotation :
  23. @noVerifier
  24. @if ( Identifier )
  25. @ifnot ( Identifier )
  26. FieldDeclaration :
  27. Identifier ArraySpecifier opt : Type ;
  28. ArraySpecifier :
  29. [ Expression ]

一个示例类:

  1. extern class JSProxy extends JSReceiver {
  2. target: JSReceiver|Null;
  3. handler: JSReceiver|Null;
  4. }

extern 表示该类是在 C++ 中定义的,而不是仅在 Torque 中定义的。

类中的字段声明隐式生成可被 CodeStubAssembler 使用的字段读写器(getter 和 setter),例如:

  1. // In TorqueGeneratedExportedMacrosAssembler:
  2. TNode<HeapObject> LoadJSProxyTarget(TNode<JSProxy> p_o);
  3. void StoreJSProxyTarget(TNode<JSProxy> p_o, TNode<HeapObject> p_v);

如上所述,在 Torque 类中定义的字段生成 C++ 代码,从而无需重复的样板访问器(boilerplate accessor)和堆访问器(heap visitor)代码。因为上面的示例使用 @generateCppClass,所以 JSProxy 的手写定义必须从生成的类模板继承,如下所示:

  1. // In js-proxy.h:
  2. class JSProxy : public TorqueGeneratedJSProxy<JSProxy, JSReceiver> {
  3. // Whatever the class needs beyond Torque-generated stuff goes here...
  4. // At the end, because it messes with public/private:
  5. TQ_OBJECT_CONSTRUCTORS(JSProxy)
  6. }
  7. // In js-proxy-inl.h:
  8. TQ_OBJECT_CONSTRUCTORS_IMPL(JSProxy)

生成的类提供了转换函数,字段访问器函数以及字段偏移量常量(例如,在这种情况下为 kTargetOffsetkHandlerOffset),这些常量表示每个字段从类的开头开始的字节偏移量。

类类型注解

推荐使用 @generateCppClass(如上例所示),但某些类仍不使用它。在这种情况下,该类应该为其字段偏移量常量包含一个 Torque 生成的宏,并且必须实现其自己的访问器和强制转换函数。 使用该宏看起来像这样:

  1. class JSProxy : public JSReceiver {
  2. public:
  3. DEFINE_FIELD_OFFSET_CONSTANTS(
  4. JSReceiver::kHeaderSize, TORQUE_GENERATED_JS_PROXY_FIELDS)
  5. // Rest of class omitted...
  6. }

@generateBodyDescriptor 使 Torque 在生成的类内抛出一个类 BodyDescriptor,它表示垃圾收集器应如何访问该对象。否则,C++ 代码必须定义自己的对象访问,或者使用现有的模式之一(例如,从 Struct 继承并在 STRUCT_LIST 中包含该类意味着该类仅应包含标记值)。

如果添加了 @generatePrint 注解,则生成器将实现 C++ 函数,该函数将打印由 Torque 布局定义的字段值。 使用 JSProxy 示例,签名将为 void TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyPrint(std::ostream& os),可以由 JSProxy 继承。

除非该类使用 @noVerifier 注解选择退出,否则 Torque 编译器还会为所有 extern 类生成验证代码。例如,上面的 JSProxy 类定义将生成一个 C++ 方法 void TorqueGeneratedClassVerifiers::JSProxyVerify(JSProxy o, Isolate* isolate),该方法根据 Torque 类型定义验证其字段是否有效。它还将在生成的类 TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyVerify 上生成相应的函数,该类从 TorqueGeneratedClassVerifiers 调用静态函数。如果要为类添加额外的验证(例如,可接受的数字值的范围,或者如果字段 bar 为非空,则要求字段 foo 为 true 等),则将 DECL_VERIFIER(JSProxy) 添加到 C++ 类(隐藏继承的 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::internal::InstanceType 中为每个类分配一个唯一值,以便 V8 在运行时可以确定 JS 堆中任何对象的类型。在大多数情况下,Torque 分配的实例类型应该足够了,但是在少数情况下,我们希望特定类的实例类型在整个构建过程,或者在实例类型分配给其超类的开始或结束时范围内,或者是可以在 Torque 之外定义的保留值范围中保持稳定。

类字段

除了上面的示例中的普通值之外,类字段也可以包含索引数据。 这是一个例子:

  1. extern class CoverageInfo extends HeapObject {
  2. const slot_count: int32;
  3. slots[slot_count]: CoverageInfoSlot;
  4. }

这意味着 CoverageInfo 实例的大小根据 slot_count 中的数据而有所不同。

与 C++ 不同,Torque 不会在字段之间隐式添加填充。 相反,如果字段未正确对齐,它将失败并发出错误。Torque 还要求强字段、弱字段和标量字段与按字段序排列的同一类别的其它字段在一起。

const 表示无法在运行时更改字段(或至少不容易更改;如果尝试设置该字段,则 Torque 将导致编译失败)。对于长度字段来说,这是一个好主意,应该非常小心地重设长度字段,因为它们将需要释放任何释放的空间,并可能导致带有线程标记的数据竞争。

字段声明开始处的 weak 表示该字段应与其它 weak 字段组合在一起,并影响常量 kEndOfStrongFieldsOffsetkStartOfWeakFieldsOffset 等可在自定义 BodyDescriptor 中使用的常量的生成。我们希望一旦 Torque 完全能够生成所有 BodyDescriptor 后就删除该关键字。如果存储在字段中的对象可能是弱引用(已设置第二个位),则应在类型中使用 Weak<T>。例如, Map 中的该字段可以包含一些强类型和一些弱类型,并且还标记为包含在 weak 部分中:

  1. weak transitions_or_prototype_info: Map|Weak<Map>|TransitionArray|
  2. PrototypeInfo|Smi;

@if@ifnot 标记应在某些构建配置中包含的字段,而在其他构建配置中则不包括。它们接受 src/torque/torque-parser.ccBuildFlags 列表中的值。

完全在 Torque 之外定义的类

有些类未在 Torque 中定义,但是 Torque 必须了解每个类,因为它负责分配实例类型。在这种情况下,可以不带任何主体声明类,并且 Torque 除实例类型外不会为它们生成任何内容。 例子:

  1. extern class OrderedHashMap extends HashTable;

形状 { #shapes }

定义 shape 看起来就像定义一个类,只不过它使用关键字 shape 而不是 classshapeJSObject 的子类型,代表对象内属性的时间点排列(具体来说,这些是“数据属性”,而不是“内部插槽”)。shape 没有自己的实例类型。具有特定形状的对象可能随时更改并丢失该形状,因为该对象可能会进入字典模式并将其所有属性移到单独的后备存储中。

结构体 { #structs }

struct 是可以轻松在一起传递的数据的集合。(与名为 Struct 的类完全无关。)像类一样,它们可以包含对数据进行操作的宏。与类不同的是,它们还支持泛型(generics)。语法类似于类:

  1. @export
  2. struct PromiseResolvingFunctions {
  3. resolve: JSFunction;
  4. reject: JSFunction;
  5. }
  6. struct ConstantIterator<T: type> {
  7. macro Empty(): bool {
  8. return false;
  9. }
  10. macro Next(): T labels _NoMore {
  11. return this.value;
  12. }
  13. value: T;
  14. }
结构体注解

标记为 @export 的任何结构体都将以可预测的名称包含在生成的文件 gen/torque-generated/csa-types-tq.h 中。该名称以 TorqueStruct 开头,因此 PromiseResolvingFunctions 成为 TorqueStructPromiseResolvingFunctions

结构体字段可以标记为 const,这意味着其不应被写入(或者说修改)。整个结构体仍然可以被覆盖。

结构体作为类的字段

可以将结构体用作类字段的类型。在那种情况下,它表示类中的打包的有序数据(否则,结构体没有对齐要求)。这对于类中的索引字段特别有用。例如,DescriptorArray 包含一个三值结构数组:

  1. struct DescriptorEntry {
  2. key: Name|Undefined;
  3. details: Smi|Undefined;
  4. value: JSAny|Weak<Map>|AccessorInfo|AccessorPair|ClassPositions;
  5. }
  6. extern class DescriptorArray extends HeapObject {
  7. const number_of_all_descriptors: uint16;
  8. number_of_descriptors: uint16;
  9. raw_number_of_marked_descriptors: uint16;
  10. filler16_bits: uint16;
  11. enum_cache: EnumCache;
  12. descriptors[number_of_all_descriptors]: DescriptorEntry;
  13. }
引用和切片

Reference<T>Slice<T> 是特殊的结构,表示指向堆对象中保存的数据的指针。它们都包含一个对象和一个偏移量。Slice<T> 也包含一个长度。除了直接构造这些结构体外,还可以使用特殊的语法:&o.x 将在对象 o 中创建对字段 x 的引用(Reference),或者如果 x 是索引字段,则创建对数据的切片(Slice)。Reference<T> 可以用 *-> 取消引用,与 C++ 一致。

Reference<T> 不应直接使用。相反,它具有两个子类型 MutableReference<T>ConstReference<T>,可以使用语法糖来引用它们:&Tconst &T

位字段结构体 { #bitfield-structs }

bitfield struct 表示打包为单个数字值的数字数据的集合。 它的语法看起来与普通 struct 类似,只是每个字段的位数有所增加。

  1. bitfield struct DebuggerHints extends uint31 {
  2. side_effect_state: int32: 2 bit;
  3. debug_is_blackboxed: bool: 1 bit;
  4. computed_debug_is_blackboxed: bool: 1 bit;
  5. debugging_id: int32: 20 bit;
  6. }

如果将位字段结构体(bitfield struct)(或任何其他数字数据)存储在 Smi 中,则可以使用 SmiTagged<T> 类型表示它。

函数指针类型 { #function-pointer-types }

函数指针(Function pointers)只能指向 Torque 中定义的内置函数,因为这保证了默认的 ABI。它们对于减小二进制代码的大小特别有用。

尽管函数指针类型是匿名的(例如在 C 中),但是可以将它们绑定到类型别名(例如在 C 中的 typedef)。

  1. type CompareBuiltinFn = builtin(implicit context: Context)(Object, Object, Object) => Number;

特殊类型 { #special-types }

关键字 voidnever 表示两种特殊类型。void 用作不返回值的可调用对象的返回类型,never 用作永不实际返回(即仅通过特殊路径退出)的可调用对象的返回类型。

瞬态类型 { #transient-types }

在 V8 中,堆对象可以在运行时更改布局。为了表示类型系统中可能发生更改或其它临时假设的对象布局,Torque 支持”瞬态类型”(transient type)的概念。在声明抽象类型时,添加关键字 transient 会将其标记为瞬态类型。

  1. // A HeapObject with a JSArray map, and either fast packed elements, or fast
  2. // holey elements when the global NoElementsProtector is not invalidated.
  3. transient type FastJSArray extends JSArray
  4. generates 'TNode<JSArray>';

例如,对于 FastJSArray,如果数组更改为字典元素,或者全局 NoElementsProtector 无效,则瞬态类型将无效。为了用 Torque 来表达这一点,请标注所有可能执行此操作的可调用对象为 transitioning。例如,调用 JavaScript 函数可以执行任意 JavaScript,因此它是 transitioning

  1. extern transitioning macro Call(implicit context: Context)
  2. (Callable, Object): Object;

在类型系统中进行控制的方式是,在 transitioning 操作中访问瞬态类型的值是非法的。

  1. const fastArray : FastJSArray = Cast<FastJSArray>(array) otherwise Bailout;
  2. Call(f, Undefined);
  3. return fastArray; // Type error: fastArray is invalid here.

枚举 { #enums }

枚举(Enumerations)提供了一种定义常量集并将其分组的方式,类似于 C++ 中的枚举类。声明由 enum 关键字引入,并遵循以下语法结构:

  1. EnumDeclaration :
  2. extern enum IdentifierName ExtendsDeclaration opt ConstexprDeclaration opt { IdentifierName list+ (, ...) opt }

一个基本的示例如下所示:

  1. extern enum LanguageMode extends Smi {
  2. kStrict,
  3. kSloppy
  4. }

该声明定义了一个新的类型 LanguageMode,其中 extends 子句指定了基础类型,即用于表示枚举值的运行时类型。在此示例中,这是 TNode<Smi>,因为这是 Smi generates 的类型。由于在枚举中未指定 constexpr 子句来替换默认名称,因此 constexpr LanguageMode 在生成的 CSA 文件中会转换为 LanguageMode。如果省略 extends 子句,Torque 将仅生成该类型的 constexpr 版本。extern 关键字告诉 Torque这个枚举由 C++ 定义。 当前,仅支持 extern 枚举。

Torque 为每个枚举项生成不同的类型和常量。它们是在与枚举名称匹配的命名空间中定义的。 生成必要的 FromConstexpr<> 专业化功能,以将条目的 constexpr 类型转换为枚举类型。为 C++ 文件中的条目生成的值是 <enum-constexpr>::<entry-name>,其中 <enum-constexpr> 是为枚举生成的constexpr 名称。在上面的示例中,它们是 LanguageMode::kStrictLanguageMode::kSloppy

Torque 的枚举与 typeswitch 构造一起很好地工作,因为这些值是使用不同的类型定义的:

  1. typeswitch(language_mode) {
  2. case (LanguageMode::kStrict): {
  3. // ...
  4. }
  5. case (LanguageMode::kSloppy): {
  6. // ...
  7. }
  8. }

如果枚举的 C++ 定义包含的值比 .tq 文件中使用的值更多,则 Torque 需要知道这一点。这是通过在最后一个条目之后附加一个 ... 来声明枚举 ‘ open‘ 来完成的。以 ExtractFixedArrayFlag 为例,在 Torque 中只有某些选项可用/使用:

  1. enum ExtractFixedArrayFlag constexpr 'CodeStubAssembler::ExtractFixedArrayFlag' {
  2. kFixedDoubleArrays,
  3. kAllFixedArrays,
  4. kFixedArrays,
  5. ...
  6. }

可调用对象 { #callables }

从概念上讲,可调用对象(Callables)类似于 JavaScript 或 C++ 中的函数,但是它们具有一些附加的语义,使它们可以以有用的方式与 CSA 代码和 V8 运行时进行交互。Torque 提供了几种不同类型的可调用对象:macrobuiltinruntimeintrinsic

  1. CallableDeclaration :
  2. MacroDeclaration
  3. BuiltinDeclaration
  4. RuntimeDeclaration
  5. IntrinsicDeclaration

macro 可调用对象 { #macro-callables }

宏(Macros)是可调用的,它对应于生成的生成 CSA(CSA-producing)的 C++ 块。macro 可以在 Torque 中完全定义,在这种情况下,CSA 代码由Torque 生成,也可以标记为 extern,在这种情况下,必须在 CodeStubAssembler 类中以手写 CSA 代码的形式提供实现。从概念上讲,考虑在 callsites 内联的可插入的 CSA 代码的 macro 是很有用的。

Torque中的 macro 声明采用以下形式:

  1. MacroDeclaration :
  2. transitioning opt macro IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt LabelsDeclaration opt StatementBlock
  3. extern transitioning opt macro IdentifierName ImplicitParameters opt ExplicitTypes ReturnType opt LabelsDeclaration opt ;

每个非 extern Torque macro 都使用它的 StatementBlock 主体在其命名空间的生成的 Assembler 类中创建 CSA 生成(CSA-generating)函数。该代码看起来与你可以在 code-stub-assembler.cc 中找到的其它代码一样,尽管可读性较低,因为它是机器生成的。标为 externmacro 没有用 Torque 编写的主体,而只是提供了手写 C++ CSA 代码的接口,以便可以从 Torque 使用。

macro 定义指定隐式和显式参数,以及可选的返回类型和可选的标签。参数和返回类型将在下面更详细地讨论,但是到目前为止,只要知道它们的工作方式与 TypeScript 参数类似就足够了,正如 TypeScript 文档的 Function Types 部分中所讨论的那样。

标签是一种异常退出 macro 的机制。它们将 1:1 映射到 CSA 标签,并作为 CodeStubAssemblerLabels*- 类型的参数添加到为 macro 生成的C ++方法中。它们的确切语义在下面进行了讨论,但是出于 macro 声明的目的,以逗号分隔的 macro 标签列表可选地带有 labels 关键字,并位于 macro 的参数列表和返回类型之后。

这是来自 base.tq 的外部和 Torque 定义(Torque-defined)的 macro 示例:

  1. extern macro BranchIfFastJSArrayForCopy(Object, Context): never
  2. labels Taken, NotTaken;
  3. macro BranchIfNotFastJSArrayForCopy(implicit context: Context)(o: Object):
  4. never
  5. labels Taken, NotTaken {
  6. BranchIfFastJSArrayForCopy(o, context) otherwise NotTaken, Taken;
  7. }

builtin 可调用对象 { #builtin-callables }

builtinmacro 类似,因为它们可以在 Torque 中完全定义,也可以标记为 extern。在基于 Torque(Torque-based)的内置实例中,内置主体用于生成 V8 内置函数,可以像其它任何 V8 内置函数一样调用它,包括自动在 builtin-definitions.h 中添加相关信息。像 macro 一样,标记为 extern 的 Torque builtin 没有基于 Torque 的主体,仅提供与现有 V8 builtin 的接口,以便可以从 Torque 代码中使用它们。

Torque中的 builtin 声明具有以下形式:

  1. MacroDeclaration :
  2. transitioning opt javascript opt builtin IdentifierName ImplicitParameters opt ExplicitParametersOrVarArgs ReturnType opt StatementBlock
  3. extern transitioning opt javascript opt builtin IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;

Torque 内置代码只有一个副本,即在生成的内置代码对象中。与 macro 不同,从 Torque 代码调用 builtin 时,不会在 callsite 内联 CSA 代码,而是会生成对内置函数的调用。

builtin 文件不能具有标签。

如果你正在编码 builtin 的实现,则可以对内置函数或运行时函数(当且仅当它是内置函数中的最终调用)进行尾调用。在这种情况下,编译器可能能够避免创建新的堆栈帧。只需在调用之前添加 tail 即可,如 tail MyBuiltin(foo, bar); 中所示。

runtime 可调用对象 { #runtime-callables }

runtimebuiltin 相似,因为它们可以将接口暴露给 Torque 外部功能。但是,runtime 提供的功能必须始终在 V8 中作为标准运行时回调来实现,而不是在 CSA 中实现。

Torque 中的 runtime 声明具有以下形式:

  1. MacroDeclaration :
  2. extern transitioning opt runtime IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;

名称为 IdentifierNameextern runtime 对应于 Runtime::kIdentifierName 指定的运行时函数。

builtin 一样,runtime 不能具有标签。

你也可以在适当时将 runtime 函数作为尾部调用。 只需在调用之前添加tail关键字即可。只需在调用之前添加 tail 关键字即可。

intrinsic 可调用对象 { #intrinsic-callables }

intrinsic 是内置的 Torque 可调用对象,可提供对内部功能的访问,而这些功能在其它情况下无法在 Torque 中实现。它们是在 Torque 中声明的,但未定义,因为该实现是由 Torque 编译器提供的。intrinsic 声明使用以下语法:

  1. IntrinsicDeclaration :
  2. intrinsic % IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt ;

在大多数情况下,“用户的” Torque 代码很少应该直接使用 intrinsic。 当前支持的内部函数(intrinsics)是:

  1. // %RawObjectCast downcasts from Object to a subtype of Object without
  2. // rigorous testing if the object is actually the destination type.
  3. // RawObjectCasts should *never* (well, almost never) be used anywhere in
  4. // Torque code except for in Torque-based UnsafeCast operators preceeded by an
  5. // appropriate type assert()
  6. intrinsic %RawObjectCast<A: type>(o: Object): A;
  7. // %RawPointerCast downcasts from RawPtr to a subtype of RawPtr without
  8. // rigorous testing if the object is actually the destination type.
  9. intrinsic %RawPointerCast<A: type>(p: RawPtr): A;
  10. // %RawConstexprCast converts one compile-time constant value to another.
  11. // Both the source and destination types should be 'constexpr'.
  12. // %RawConstexprCast translate to static_casts in the generated C++ code.
  13. intrinsic %RawConstexprCast<To: type, From: type>(f: From): To;
  14. // %FromConstexpr converts a constexpr value into into a non-constexpr
  15. // value. Currently, only conversion to the following non-constexpr types
  16. // are supported: Smi, Number, String, uintptr, intptr, and int32
  17. intrinsic %FromConstexpr<To: type, From: type>(b: From): To;
  18. // %Allocate allocates an unitialized object of size 'size' from V8's
  19. // GC heap and "reinterpret casts" the resulting object pointer to the
  20. // specified Torque class, allowing constructors to subsequently use
  21. // standard field access operators to initialize the object.
  22. // This intrinsic should never be called from Torque code. It's used
  23. // internally when desugaring the 'new' operator.
  24. intrinsic %Allocate<Class: type>(size: intptr): Class;

builtinruntime 一样,intrinsic 不能具有标签。

显式参数 { #explicit-parameters }

Torque 定义(Torque-defined)的可调用对象的声明,例如,Torque macrobuiltin 具有明确的参数列表。它们是标识符(identifier)和类型对的列表,使用的语法让人联想到带类型的 TypeScript 函数参数列表,但 Torque 不支持可选参数或默认参数。此外,如果内建函数使用 V8 的内部 JavaScript调用约定(例如,用 javascript 关键字标记),则 Torque 实现的(Torque-implement )builtin 可以选择支持剩余(rest)参数。

  1. ExplicitParameters :
  2. ( ( IdentifierName : TypeIdentifierName ) list* )
  3. ( ( IdentifierName : TypeIdentifierName ) list+ (, ... IdentifierName ) opt )

举个例子:

  1. javascript builtin ArraySlice(
  2. (implicit context: Context)(receiver: Object, ...arguments): Object {
  3. // …
  4. }

隐式参数 { #implicit-parameters }

Torque 可调用对象可以使用类似于 Scala 的隐式参数 的方式指定隐式参数:

  1. ImplicitParameters :
  2. ( implicit ( IdentifierName : TypeIdentifierName ) list* )

具体来说:macro 除了可以声明显式参数外,还可以声明隐式参数:

  1. macro Foo(implicit context: Context)(x: Smi, y: Smi)

映射到 CSA 时,隐式参数和显式参数被视为相同的,并形成联合参数列表。

callsite 未提及隐式参数,而是隐式传递参数:Foo(4, 5)。为此,必须在提供名为 context 的值的上下文中调用 Foo(4, 5)。 例子:

  1. macro Bar(implicit context: Context)() {
  2. Foo(4, 5);
  3. }

与 Scala 相比,如果隐式参数的名称不同,则我们禁止这样做。

由于重载解析(overload resolution)会导致混乱的行为,因此我们确保隐式参数根本不会影响重载解析。即:在比较重载集合的候选者时,我们不考虑 call-site 上可用的隐式绑定。仅在找到单个最佳重载之后,我们才检查隐式参数的隐式绑定是否可用。

在显式参数中保留隐式参数与 Scala 有所不同,但可以更好地映射到 CSA 中的现有约定,使其首先具有 context 参数。

js-implicit

对于在 Torque 中定义的具有 JavaScript 链接的内置程序,应使用关键字 js-implicit 而不是 implicit 关键字。参数仅限于调用约定的以下四个组成部分:

  • context: NativeContext
  • receiver: JSAny (this in JavaScript)
  • target: JSFunction (arguments.callee in JavaScript)
  • newTarget: JSAny (new.target in JavaScript)

它们不必全部声明,而只需要声明你要使用的即可。 例如,这是我们用于 Array.prototype.shift 的代码:

  1. // https://tc39.es/ecma262/#sec-array.prototype.shift
  2. transitioning javascript builtin ArrayPrototypeShift(
  3. js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  4. ...

请注意,context 参数是 NativeContext。这是因为 V8 中的内置程序始终在其闭包中嵌入本机上下文(native context)。使用 js-implicit 约定对此进行编码,使程序员可以消除从函数上下文中加载本机上下文的操作。

重载解析 { #overload-resolution }

Torque macro 和运算符(它们只是 macro 的别名)允许参数类型重载(overloading)。重载规则是受 C++ 启发的:如果重载严格优于所有替代方法,则选择重载。这意味着它必须在至少一个参数的情况下严格地更好,而在所有多个参数的情况下都必须更好或同样好。

比较两个重载的一对对应的参数时…

  • …它们被认为同样好,如果:
    • 它们是一致的;
    • 两者都需要一些隐式转换。
  • …如果满足以下条件,则认为第一种更好:
    • 它是另一个的严格子类型;
    • 它不需要隐式转换,而另一个则需要。

如果没有重载严格地优于所有替代方法,则将导致编译错误。

延迟块 { #deferred-blocks }

可以选择将语句块标记为 deferred,这是向编译器发出的信号,表明它的输入频率降低了。编译器可以选择将这些块放置在函数的末尾,从而提高了非延迟(non-deferred)代码区域的缓存局部性(cache locality)。例如,在 Array.prototype.forEach 实现的以下代码中,我们希望保留在“快速”路径上,并且很少采用 Bailout 方案:

  1. let k: Number = 0;
  2. try {
  3. return FastArrayForEach(o, len, callbackfn, thisArg)
  4. otherwise Bailout;
  5. }
  6. label Bailout(kValue: Smi) deferred {
  7. k = kValue;
  8. }

这是另一个示例,其中将字典元素(DICTIONARY_ELEMENTS)的情形标记为 deferred,以改善更相似情形的代码生成(来自 Array.prototype.join 实现):

  1. if (IsElementsKindLessThanOrEqual(kind, HOLEY_ELEMENTS)) {
  2. loadFn = LoadJoinElement<FastSmiOrObjectElements>;
  3. } else if (IsElementsKindLessThanOrEqual(kind, HOLEY_DOUBLE_ELEMENTS)) {
  4. loadFn = LoadJoinElement<FastDoubleElements>;
  5. } else if (kind == DICTIONARY_ELEMENTS)
  6. deferred {
  7. const dict: NumberDictionary =
  8. UnsafeCast<NumberDictionary>(array.elements);
  9. const nofElements: Smi = GetNumberDictionaryNumberOfElements(dict);
  10. // <etc>...

将 CSA 代码移植到 Torque { #porting-csa-code-to-torque }

移植了 Array.of 的补丁 提供了将 CSA 代码移植到 Torque 的最小示例。