V8 Torque 用户手册
V8 Torque 是一种语言,它允许为 V8 项目做出贡献的开发人员通过关注他们对 VM 的更改的意图来表达 VM 中的更改,而不是专注于无关的实现细节。该语言的设计足够简单,可以轻松地将ECMAScript 规范直接转换为 V8 中的实现,但功能强大到足以以稳健的方式表达低级 V8 优化技巧,例如基于针对特定对象形状的测试创建快速路径。
Torque 对 V8 工程师和 JavaScript 开发人员来说很熟悉,它结合了类似 TypeScript 的语法,简化了 V8 代码的编写和理解,并结合了反映CodeStubAssembler
中已有的概念的语法和类型。通过强大的类型系统和结构化控制流,Torque 通过构造确保正确性。Torque 的表达能力足以表达几乎所有目前在 V8 内置函数中找到的功能。它还与 CodeStubAssembler
内置函数和用 C++ 编写的 macro
非常互操作,允许 Torque 代码使用手工编写的 CSA 功能,反之亦然。
Torque 提供语言结构来表示 V8 实现的高级、语义丰富的片段,Torque 编译器使用 CodeStubAssembler
将这些片段转换为高效的汇编代码。Torque 的语言结构和 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
中。
为了让您体验一下这种语言,让我们编写一个打印“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
都会导致生成以下文件path/to/file-tq-csa.cc
和path/to/file-tq-csa.h
包含生成的 CSA 宏。path/to/file-tq.inc
将包含在相应的头文件path/to/file.h
中,其中包含类定义。path/to/file-tq-inl.inc
将包含在相应的内联头文件path/to/file-inl.h
中,其中包含类定义的 C++ 访问器。path/to/file-tq.cc
包含生成的堆验证器、打印机等。
Torque 编译器还会生成各种其他已知的
.h
文件,这些文件旨在供 V8 构建使用。然后,
gn
构建将步骤 1 中生成的-csa.cc
文件编译到mksnapshot
可执行文件中。当
mksnapshot
运行时,所有 V8 的内置函数都会生成并打包到快照文件中,包括在 Torque 中定义的那些内置函数以及使用 Torque 定义的功能的任何其他内置函数。V8 的其余部分已构建。所有 Torque 编写的内置函数都可以通过链接到 V8 的快照文件访问。它们可以像任何其他内置函数一样被调用。此外,
d8
或chrome
可执行文件还包含与类定义直接相关的生成的编译单元。
从图形上看,构建过程如下所示
Torque 工具 #
Torque 提供了基本的工具和开发环境支持。
- 有一个Visual Studio Code 插件用于 Torque,它使用自定义语言服务器来提供诸如转到定义之类的功能。
- 还有一个格式化工具,应该在更改
.tq
文件后使用:tools/torque/format-torque.py -i <filename>
解决涉及 Torque 的构建问题 #
为什么要了解这些?了解 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++ 构建将失败。 - 即使
mksnapshot
成功构建,它也可能在执行期间失败。这可能是因为 Turbofan 无法编译生成的 CSA 代码,例如因为 Torquestatic_assert
无法由 Turbofan 验证。此外,在快照创建期间运行的 Torque 提供的内置函数可能存在错误。例如,Array.prototype.splice
(一个 Torque 编写的内置函数)作为 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
:编译时与运行时 #
了解 Torque 构建过程对于理解 Torque 语言中的核心功能 constexpr
也很重要。
Torque 允许在运行时(即当 V8 内置函数作为执行 JavaScript 的一部分执行时)评估 Torque 代码中的表达式。但是,它还允许在编译时(即作为 Torque 构建过程的一部分,以及在 V8 库和 d8
可执行文件甚至创建之前)评估表达式。
Torque 使用 constexpr
关键字来指示必须在构建时评估表达式。它的用法与C++ 的 constexpr
有些类似:除了借用 constexpr
关键字及其语法的一部分之外,Torque 还类似地使用 constexpr
来指示编译时和运行时评估之间的区别。
但是,Torque 的 constexpr
语义有一些细微的差别。在 C++ 中,constexpr
表达式可以完全由 C++ 编译器评估。在 Torque 中,constexpr
表达式不能完全由 Torque 编译器评估,而是映射到 C++ 类型、变量和表达式,这些类型、变量和表达式可以在运行 mksnapshot
时(并且必须)完全评估。从 Torque 编写者的角度来看,constexpr
表达式不会生成在运行时执行的代码,因此从这个意义上说它们是编译时的,即使它们在技术上是由 mksnapshot
运行的 Torque 外部的 C++ 代码评估的。因此,在 Torque 中,constexpr
本质上意味着“mksnapshot
时间”,而不是“编译时间”。
与泛型结合使用,constexpr
是一种强大的 Torque 工具,可用于自动生成多个非常高效的专用内置函数,这些内置函数在 V8 开发人员可以提前预期的少量特定细节方面彼此不同。
文件 #
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++ 类型。
以下是 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 | A
A | (B | C) = (A | B) | C
A | 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
表示此类是在 C++ 中定义的,而不是仅在 Torque 中定义的。
类中的字段声明隐式地生成字段 getter 和 setter,这些 getter 和 setter 可以从 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
使 Torque 在生成的类中发出一个类 BodyDescriptor
,该类表示垃圾收集器应如何访问该对象。否则,C++ 代码必须要么定义自己的对象访问,要么使用现有的模式之一(例如,从 Struct
继承并将类包含在 STRUCT_LIST
中意味着该类预计仅包含标记值)。
如果添加了 @generatePrint
注释,则生成器将实现一个 C++ 函数,该函数将根据 Torque 布局打印字段值。使用 JSProxy 示例,签名将是 void TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyPrint(std::ostream& os)
,JSProxy 可以继承该签名。
Torque 编译器还为所有 extern
类生成验证代码,除非类使用 @noVerifier
注释选择退出。例如,上面的 JSProxy 类定义将生成一个 C++ 方法 void TorqueGeneratedClassVerifiers::JSProxyVerify(JSProxy o, Isolate* isolate)
,该方法验证其字段是否根据 Torque 类型定义有效。它还将在生成的类上生成一个相应的函数 TorqueGeneratedJSProxy<JSProxy, JSReceiver>::JSProxyVerify
,该函数调用 TorqueGeneratedClassVerifiers
中的静态函数。如果您想为类添加额外的验证(例如,数字的允许值范围,或者字段 foo
为真如果字段 bar
非空等),则在 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::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
样式的弱引用(第二位设置),则应在类型中使用 Weak<T>
,并且不应使用 weak
关键字。此规则仍然有一些例外,例如来自 Map
的此字段,它可以包含一些强类型和一些弱类型,并且也标记为 weak
以包含在弱部分中
weak transitions_or_prototype_info: Map|Weak<Map>|TransitionArray|
PrototypeInfo|Smi;
@if
和 @ifnot
标记应包含在某些构建配置中但不包含在其他配置中的字段。它们接受来自 BuildFlags
中列表的值,位于 src/torque/torque-parser.cc
中。
完全在 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
包含一个三值结构体数组
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
。对于引用和切片,都有常量和可变版本。对于引用,这些类型分别写为 &T
和 const &T
,用于可变引用和常量引用。可变性是指它们指向的数据,并且可能不全局保持,也就是说,您可以创建指向可变数据的常量引用。对于切片,没有类型的特殊语法,两个版本分别写为 ConstSlice<T>
和 MutableSlice<T>
。引用可以使用 *
或 ->
取消引用,与 C++ 一致。
指向未标记数据的引用和切片也可以指向堆外数据。
位域结构体 #
位域结构体
表示打包到单个数值中的数值数据的集合。它的语法看起来类似于普通的 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
表示的特殊类型。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
generates
的类型。一个 constexpr LanguageMode
会转换为 LanguageMode
在生成的 CSA 文件中,因为枚举上没有指定 constexpr
子句来替换默认名称。
如果省略了 extends
子句,Torque 将只生成类型的 constexpr
版本。extern
关键字告诉 Torque 此枚举有一个 C++ 定义。目前,只支持 extern
枚举。
Torque 为枚举的每个条目生成一个不同的类型和常量。这些定义在
与枚举名称匹配的命名空间内。FromConstexpr<>
的必要特化是
生成用于从条目的 constexpr
类型转换为枚举类型。在 C++ 文件中为条目生成的 value 是 <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 需要知道这一点。这是通过在最后一个条目后附加 ...
来声明枚举为“open”来完成的。例如,考虑 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
,在这种情况下,实现必须作为手写的 CSA 代码提供在 CodeStubAssembler 类中。从概念上讲,将 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 中没有主体,只是为手写的 C++ CSA 代码提供接口,以便它可以从 Torque 中使用。
macro
定义指定了隐式和显式参数、可选的返回类型和可选的标签。参数和返回类型将在下面更详细地讨论,但现在知道它们的工作方式有点像 TypeScript 参数就足够了,正如 TypeScript 文档的函数类型部分所讨论的那样 这里。
标签是从 macro
异常退出的机制。它们与 CSA 标签一一对应,并作为 CodeStubAssemblerLabels*
类型的参数添加到为 macro
生成的 C++ 方法中。它们的精确语义将在下面讨论,但为了 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
类似于 macro
,因为它们可以完全在 Torque 中定义或标记为 extern
。在基于 Torque 的内置情况下,内置函数的主体用于生成一个 V8 内置函数,该函数可以像任何其他 V8 内置函数一样调用,包括自动在 builtin-definitions.h
中添加相关信息。与 macro
类似,标记为 extern
的 Torque builtin
没有基于 Torque 的主体,只是为现有的 V8 builtin
提供一个接口,以便它们可以从 Torque 代码中使用。
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 内置函数的代码只有一份,它位于生成的内置代码对象中。与 macro
不同,当从 Torque 代码调用 builtin
时,CSA 代码不会在调用点内联,而是生成对内置函数的调用。
builtin
不能有标签。
如果您正在编码 builtin
的实现,您可以制作一个 尾调用 到一个内置函数或一个运行时函数,当且仅当它是内置函数中的最后一个调用时。在这种情况下,编译器可能能够避免创建新的堆栈帧。只需在调用之前添加 tail
,如 tail MyBuiltin(foo, bar);
。
runtime
可调用对象 #
runtime
类似于 builtin
,因为它们可以向 Torque 公开外部功能的接口。但是,它们不是在 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 相反,如果隐式参数的名称不相同,我们禁止这样做。
由于重载解析会导致令人困惑的行为,我们确保隐式参数根本不影响重载解析。也就是说:在比较重载集的候选者时,我们不考虑调用点处的可用隐式绑定。只有在我们找到一个最佳重载后,我们才会检查隐式参数的隐式绑定是否可用。
将隐式参数放在显式参数的左侧不同于 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
代码
// 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++ 的启发:如果一个重载严格优于所有备选方案,则选择该重载。这意味着它必须至少在一个参数上严格优于其他参数,并且在所有其他参数上优于或等于其他参数。
在比较两个重载的对应参数对时…
- …如果它们被认为是同等好的
- 它们是相等的;
- 两者都需要一些隐式转换。
- …如果一个被认为更好
- 它是另一个的严格子类型;
- 它不需要隐式转换,而另一个需要。
如果没有一个重载严格优于所有备选方案,这将导致编译错误。
延迟块 #
一个语句块可以选择标记为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 的一个最小示例。