Руководство пользователя V8 Torque
V8 Torque — это язык, который позволяет разработчикам, участвующим в проекте V8, выражать изменения в виртуальной машине, сосредотачиваясь на намерении своих изменений, а не на несвязанных деталях реализации. Язык был разработан таким образом, чтобы быть достаточно простым для прямого перевода спецификации ECMAScript в реализацию V8, но в то же время достаточно мощным, чтобы выразить низкоуровневые оптимизации V8, такие как создание быстрых путей на основе тестов на определенные формы объектов.
Torque будет знаком инженерам V8 и разработчикам JavaScript, сочетая синтаксис, похожий на TypeScript, который облегчает как написание, так и понимание кода V8, с синтаксисом и типами, отражающими концепции, уже привычные в CodeStubAssembler
. Благодаря сильной системе типов и структурированному управлению потоком Torque обеспечивает корректность программы на стадии разработки. Выразительные возможности Torque позволяют реализовать почти весь функционал, в настоящее время присутствующий в встроенных функциях V8. Также он легко взаимодействует с встроенными функциями и macro
, написанными на C++ в CodeStubAssembler
, что позволяет Torque-коду использовать функциональность CSA и наоборот.
Torque предоставляет языковые конструкции для представления высокоуровневых, семантически насыщенных частиц реализации V8, а компилятор Torque преобразует их в эффективный машинный код с использованием CodeStubAssembler
. Как сама структура языка Torque, так и проверки ошибок компилятора Torque гарантируют корректность программ, исключая трудоемкость и вероятность ошибок, связанных с прямым использованием CodeStubAssembler
. Традиционно, написание оптимального кода с использованием CodeStubAssembler
требовало от инженеров V8 высокого уровня специализированных знаний, большая часть которых никогда не была формально зафиксирована в письменной документации, чтобы избегать тонких ошибок в реализации. Без этих знаний обучение эффективной записи встроенных функций было трудоемким. Даже обладая необходимыми знаниями, неочевидные и неконтролируемые подводные камни часто приводили к ошибкам корректности или безопасности программ. С Torque многие из этих подводных камней можно избежать и распознать автоматически с помощью компилятора Torque.
Начало работы
Большинство исходного кода, написанного на Torque, проверяется в репозитории V8 в каталоге src/builtins
с расширением файла .tq
. Определения классов, размещенных на куче V8, находятся рядом с их определениями на C++ в .tq
файлах с теми же именами, что и соответствующие файлы на C++ в src/objects
. Сам компилятор Torque можно найти в src/torque
. Тесты функциональности Torque находятся в test/torque
, test/cctest/torque
, и test/unittests/torque
.
Чтобы познакомиться с языком, давайте напишем встроенную функцию V8, которая печатает "Hello World!". Для этого мы добавим macro
в Torque в тестовом случае и вызовем его из тестовой среды 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 не создает машинный код напрямую, а генерирует C++ код, который вызывает существующий интерфейс CodeStubAssembler
в V8. CodeStubAssembler
использует компилятор TurboFan для генерации эффективного кода. Таким образом, процесс компиляции Torque состоит из нескольких этапов:
-
На этапе сборки с помощью
gn
компилятор Torque обрабатывает все файлы*.tq
. Каждый файл Torquepath/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
затем компилирует сгенерированные файлы-csa.cc
из шага 1 в исполняемый файл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
. Определения, помеченные ключевым словомextern
в коде Torque, сигнализируют компилятору Torque о том, что определение требуемой функциональности находится в C++. В настоящее время связь между определениямиextern
из файлов.tq
и кодом C++, к которому они относятся, является слабой, и на этапе компилирования Torque этой связи не проверяется. Когда определенияextern
не соответствуют (или в самых тонких случаях маскируют) функциональность, к которой они обращаются, в заголовочном файлеcode-stub-assembler.h
или других заголовках V8, сборка C++ дляmksnapshot
завершается неудачей. - Даже если
mksnapshot
успешно собирается, он может завершиться неудачей во время выполнения. Это может произойти, например, если Turbofan не удается скомпилировать сгенерированный код CSA, потому что утверждениеstatic_assert
в Torque не может быть проверено с помощью Turbofan. Также встроенная функция Torque, которая выполняется во время создания снапшота, может содержать ошибку. Например, встроенная функцияArray.prototype.splice
, написанная на Torque, вызывается в процессе инициализации снапшота JavaScript для настройки стандартной среды JavaScript. Если в реализации есть ошибка,mksnapshot
завершает выполнение с аварией. Когдаmksnapshot
завершается с ошибкой, иногда полезно вызватьmksnapshot
, передав флаг--gdb-jit-full
, который генерирует дополнительную информацию для отладки, предоставляя полезный контекст, например, имена встроенных функций Torque в стеке вызововgdb
. - Конечно, даже если код, написанный на Torque, проходит через
mksnapshot
, он может содержать ошибки или завершаться аварией. Добавление тестовых случаев вtorque-test.tq
иtorque-test.cc
— хороший способ убедиться, что ваш код на Torque работает так, как вы ожидаете. Если ваш код на Torque все же аварийно завершается вd8
илиchrome
, флаг--gdb-jit-full
снова становится очень полезным.
constexpr
: время компиляции против времени выполнения
Понимание процесса сборки Torque также важно для понимания ключевой функции языка Torque: constexpr
.
Torque позволяет оценивать выражения в коде Torque в режиме выполнения (то есть когда встроенные функции V8 выполняются как часть выполнения JavaScript). Однако он также позволяет выполнять выражения во время компиляции (то есть в рамках процесса сборки Torque и до того, как библиотека V8 и исполняемый файл d8
будут созданы).
Torque использует ключевое слово constexpr
, чтобы указать, что выражение должно быть вычислено на этапе сборки. Его использование в некоторой степени аналогично ключевому слову constexpr
в C++: помимо заимствования ключевого слова constexpr
и некоторых его синтаксических конструкций из C++, Torque также использует constexpr
для указания различия между вычислением на этапе компиляции и в процессе выполнения.
Тем не менее, есть некоторые тонкие различия в семантике использования constexpr
в Torque. В C++ выражения constexpr
могут быть полностью вычислены компилятором C++. В Torque выражения constexpr
не могут быть полностью вычислены компилятором Torque, но вместо этого отображаются в типы, переменные и выражения C++, которые могут (и должны) быть полностью вычислены при выполнении mksnapshot
. С точки зрения автора Torque выражения constexpr
не генерируют код, исполняемый в процессе выполнения, поэтому в этом смысле они относят себя к этапу компиляции, несмотря на то, что технически они вычисляются кодом C++ вне Torque, который выполняет mksnapshot
. Таким образом, в Torque constexpr
фактически означает "этап mksnapshot
", а не "этап компиляции".
В сочетании с дженериками constexpr
является мощным инструментом Torque, который можно использовать для автоматизации генерации множества специализированных встроенных функций, отличающихся друг от друга небольшим количеством конкретных деталей, которые разработчики V8 могут предвидеть заранее.
Файлы
Код 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
опционально указывает строковый литерал, который соответствует типу TNode
C++, используемому в коде CodeStubAssembler
для хранения значения времени выполнения своего типа. ConstexprDeclaration
является строковым литералом, который указывает тип C++, соответствующий версии типа Torque constexpr
для оценки на этапе сборки (mksnapshot
).
Пример из base.tq
для 31- и 32-битных типов целых чисел Torque:
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 объединенные типы.
Типы классов
Типы классов позволяют определять, выделять и манипулировать структурированными объектами на куче V8 GC из Torque-кода. Каждый тип класса Torque должен соответствовать подклассу HeapObject в C++ коде. Для минимизации затрат на поддержку шаблонного кода доступа к объектам между C++ и реализацией Torque в V8 определения классов 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.
Объявления полей в классах автоматически генерируют геттеры и сеттеры полей, которые могут быть использованы из CodeStubAssembler, например:
// В TorqueGeneratedExportedMacrosAssembler:
TNode<HeapObject> LoadJSProxyTarget(TNode<JSProxy> p_o);
void StoreJSProxyTarget(TNode<JSProxy> p_o, TNode<HeapObject> p_v);
Как описано выше, поля, определенные в классах Torque, генерируют C++ код, который устраняет необходимость повторяющегося шаблонного кода доступа и посетителя кучи. Ручное определение JSProxy должно наследовать от сгенерированного шаблона класса, например:
// В js-proxy.h:
class JSProxy : public TorqueGeneratedJSProxy<JSProxy, JSReceiver> {
// Все необходимое для класса, кроме сгенерированных компонентов Torque, можно добавить здесь...
// В конце, так как это влияет на доступ public/private:
TQ_OBJECT_CONSTRUCTORS(JSProxy)
}
// В 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
, генератор реализует функцию 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
ненулевое и т. д.), добавьте DECL_VERIFIER(JSProxy)
в C++ класс (который скрывает унаследованный метод JSProxyVerify
) и реализуйте его в src/objects-debug.cc
. Первый шаг любого такого пользовательского проверяющего должен быть вызов сгенерированной функции проверки, например, TorqueGeneratedClassVerifiers::JSProxyVerify(*this, isolate);
. (Чтобы выполнять эти проверки до и после каждого процесса сборки мусора, создайте сборку с v8_enable_verify_heap = true
и запустите с параметром --verify-heap
.)
@abstract
указывает, что сам класс не создается и не имеет собственного типа экземпляра: типы экземпляров, которые логически принадлежат классу, являются типами экземпляров производных классов.
Аннотация @export
заставляет компилятор Torque генерировать конкретный C++ класс (например, JSProxy
в приведенном выше примере). Это полезно только если вы не хотите добавлять какую-либо функциональность на C++ сверх той, что предоставляется сгенерированным кодом Torque. Ее нельзя использовать вместе с 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;
Формы (Shapes)
Определение shape
выглядит точно так же, как определение class
, за исключением того, что используется ключевое слово shape
вместо class
. shape
является подтипом JSObject
, представляющим момент времени расположения свойств внутри объекта (в спецификации они называются "data properties" вместо "internal slots"). 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
Любой struct, помеченный как @export
, будет включен с предсказуемым именем в сгенерированный файл gen/torque-generated/csa-types.h
. Имя будет предварено TorqueStruct
, так что PromiseResolvingFunctions
станет TorqueStructPromiseResolvingFunctions
.
Поля struct могут быть помечены как const
, что означает, что их не следует записывать. Весь struct все ещё может быть перезаписан.
Struct как поля класса
Struct может использоваться как тип поля класса. В этом случае он представляет пакетированные, упорядоченные данные внутри класса (в противном случае struct не имеют требований к выравниванию). Это особенно полезно для индексации полей в классах. Например, DescriptorArray
содержит массив трехзначных 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
создаст Reference
на поле x
внутри объекта o
, или Slice
на данные, если x
является индексированным полем. Как для ссылок, так и для срезов существуют const и изменяемые версии. Для ссылок эти типы пишутся как &T
и const &T
для изменяемых и постоянных ссылок соответственно. Изменяемость относится к данным, на которые они указывают, и может не сохраняться глобально, то есть можно создать const ссылки на изменяемые данные. Для срезов нет специального синтаксиса для типов, и две версии пишутся как ConstSlice<T>
и MutableSlice<T>
. Ссылки можно разименовать с помощью *
или ->
, как в C++.
Ссылки и срезы на неразмеченные данные также могут указывать на данные вне кучи.
Struct битовых полей
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;
}
Если struct битовых полей (или любые другие числовые данные) хранятся внутри Smi, они могут быть представлены с использованием типа SmiTagged<T>
.
Типы указателей на функции
Указатели на функции могут указывать только на встроенные функции, определенные в Torque, так как это гарантирует стандартный ABI. Они особенно полезны для уменьшения размера бинарного кода.
Хотя типы указателей на функции анонимны (как в C), они могут быть связаны с псевдонимом типа (как typedef
в C).
type CompareBuiltinFn = builtin(implicit context: Context)(Object, Object, Object) => Number;
Специальные типы
Существуют два специальных типа, указанных ключевыми словами void
и never
. void
используется как тип возврата для вызываемых объектов, которые не возвращают значения, а never
используется как тип возврата для вызываемых объектов, которые на самом деле никогда не возвращают (т.е. выходят только через исключительные пути).
Временные типы
Во V8 объекты кучи могут изменять схемы в процессе выполнения. Чтобы выразить схемы объектов, которые могут изменяться или другие временные предположения в системе типов, Torque поддерживает концепцию «временного типа». При объявлении абстрактного типа добавление ключевого слова transient
помечает его как временный тип.
// Объект кучи с картой JSArray и либо быстрыми упакованными элементами, либо
// быстрыми дырявыми элементами, если глобальный 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) иначе Bailout;
Call(f, Undefined);
return fastArray; // Ошибка типа: fastArray здесь недействителен.
Перечисления
Перечисления предоставляют средство для определения набора констант и их группировки под именем, аналогичным классу enum в C++. Объявление вводится с помощью ключевого слова enum
и соответствует следующей синтаксической структуре:
EnumDeclaration :
extern enum IdentifierName ExtendsDeclaration opt ConstexprDeclaration opt { IdentifierName list+ (, ...) opt }
Простой пример выглядит следующим образом:
extern enum LanguageMode extends Smi {
kStrict,
kSloppy
}
Это объявление определяет новый тип LanguageMode
, где extends
указывает базовый тип, то есть тип времени выполнения, используемый для представления значения enum. В этом примере это TNode<Smi>
, так как это то, что тип Smi
создает
. Тип constexpr LanguageMode
преобразуется в LanguageMode
в сгенерированных файлах CSA, так как на enum не задано ключевое слово constexpr
для замены имени по умолчанию.
Если компонент extends
опущен, Torque будет генерировать только версию типа constexpr
. Ключевое слово extern
указывает Torque, что существует определение этой enum в C++. На данный момент поддерживаются только extern
перечисления.
Torque генерирует отдельный тип и константу для каждого элемента enum. Они определены внутри пространства имен, соответствующего имени enum. Необходимые специализации FromConstexpr<>
генерируются для преобразования типов constexpr
элемента в тип enum. Значение, генерируемое для элемента в файлах C++, имеет форму <enum-constexpr>::<entry-name>
, где <enum-constexpr>
— это имя constexpr
, созданное для enum. В приведенном выше примере это LanguageMode::kStrict
и LanguageMode::kSloppy
.
Перечисления Torque отлично работают вместе с конструкцией typeswitch
, так как значения определены с использованием отдельных типов:
typeswitch(language_mode) {
case (LanguageMode::kStrict): {
// ...
}
case (LanguageMode::kSloppy): {
// ...
}
}
Если C++ определение enum содержит больше значений, чем те, которые используются в .tq
файлах, Torque должен знать об этом. Это делается путем объявления enum как 'открытого', добавляя ...
после последнего элемента. Рассмотрим 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, который внедряется в местах вызова.
Объявления 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
для создания функции, генерирующей CSA, в сгенерированном классе Assembler
своего пространства имен. Этот код выглядит так же, как и другой код, который можно найти в code-stub-assembler.cc
, хотя он менее читаем из-за машинной генерации. macro
, помеченные как extern
, не имеют тела, написанного в Torque, и просто предоставляют интерфейс к написанному вручную C++ CSA-коду, чтобы он был использован из Torque.
macro
определения указывают скрытые и явные параметры, необязательный тип возвращаемого значения и необязательные метки. Параметры и типы возвращаемых значений будут обсуждены подробнее ниже, но пока достаточно знать, что они работают примерно как параметры TypeScript, описанные в разделе функций документации TypeScript здесь.
Метки являются механизмом для исключительного выхода из macro
. Они соответствуют 1:1 меткам CSA и добавляются в качестве параметров типа CodeStubAssemblerLabels*
в 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 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
существует только одна копия кода, которая содержится в сгенерированном объекте кода builtin
. В отличие от macro
, при вызове builtin
из кода Torque, CSA-код не вставляется в место вызова, а вместо этого генерируется вызов к builtin
.
builtin
не могут иметь метки.
Если вы пишете реализацию builtin
, вы можете создать хвостовой вызов к builtin
или функции среды выполнения, только если это последний вызов в builtin
. Компилятор в данном случае может избежать создания нового стека. Просто добавьте tail
перед вызовом, например, tail MyBuiltin(foo, bar);
.
runtime
вызываемые функции
runtime
функции схожи с builtin
тем, что они могут предоставлять интерфейс к внешнему функционалу для Torque. Однако вместо реализации в CSA, функциональность, предоставляемая runtime
, всегда должна быть реализована в V8 как стандартный обратный вызов среды выполнения.
Объявления runtime
в Torque имеют следующую форму:
MacroDeclaration :
extern transitioning opt runtime IdentifierName ImplicitParameters opt ExplicitTypesOrVarArgs ReturnType opt ;
extern runtime
с именем IdentifierName соответствует функции среды выполнения, заданной как Runtime::kIdentifierName
.
Как и builtin
, runtime
не могут иметь метки.
Вы также можете вызвать функцию runtime
как хвостовой вызов, если это уместно. Просто добавьте ключевое слово tail
перед вызовом.
Объявления функций среды выполнения часто размещаются в пространстве имен runtime
. Это позволяет избежать путаницы с builtin
того же имени и упрощает понимание того, что в данном случае вызывается функция среды выполнения. Мы могли бы рассмотреть вопрос об обязательности этого подхода.
intrinsic
вызываемые функции
intrinsic
это встроенные вызываемые функции Torque, которые предоставляют доступ к внутреннему функционалу, который нельзя реализовать непосредственно в Torque. Они объявляются в Torque, но не определяются, так как их реализация предоставляется компилятором Torque. Объявления intrinsic
используют следующую грамматику:
IntrinsicDeclaration :
intrinsic % IdentifierName ImplicitParameters opt ExplicitParameters ReturnType opt ;
В основном, обычный код на Torque редко должен напрямую использовать intrinsic
.
Ниже приведены некоторые поддерживаемые intrinsic
:
// %RawObjectCast понижает тип от Object до подтипа Object без
// строгой проверки, является ли объект фактически целевым типом.
// RawObjectCasts *никогда* (ну, почти никогда) не должны использоваться
// в коде Torque, за исключением UnsafeCast операторов на основе Torque,
// предваряемых соответствующим type assert()
intrinsic %RawObjectCast<A: type>(o: Object): A;
// %RawPointerCast понижает тип от RawPtr до подтипа RawPtr без
// строгой проверки, является ли объект фактически целевым типом.
intrinsic %RawPointerCast<A: type>(p: RawPtr): A;
// %RawConstexprCast преобразует одно значение времени компиляции в другое.
// И исходный, и целевой типы должны быть 'constexpr'.
// %RawConstexprCast транслируется в static_cast в сгенерированном C++ коде.
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 выделяет неинициализированный объект размером 'size' из кучи
// V8 GC и "reinterpret casts" указатель на объект в целевой тип.
// указан Torque класс, позволяющий конструкторам впоследствии использовать
// стандартные операторы доступа к полям для инициализации объекта.
// Этот intrinsic никогда не должен вызываться из кода Torque. Он используется
// внутренне при упрощении оператора 'new'.
intrinsic %Allocate<Class: type>(size: intptr): Class;
Как builtin
и runtime
, intrinsic
не могут иметь метки.
Явные параметры
Объявления вызываемых объектов, определенных Torque, например, Torque macro
и builtin
, имеют явные списки параметров. Они представляют собой список пар идентификаторов и типов с синтаксисом, напоминающим список параметров функций с типизацией в TypeScript, за исключением того, что Torque не поддерживает необязательные параметры или параметры по умолчанию. Кроме того, builtin
, реализованный в Torque, может опционально поддерживать параметры остатка, если builtin
использует внутренний JavaScript-конвенцию вызова V8 (например, помечен ключевым словом 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
Для builtin
с JavaScript-связью, определенной в Torque, вместо implicit
следует использовать ключевое слово js-implicit
. Аргументы ограничиваются четырьмя компонентами соглашения вызова:
- context:
NativeContext
- receiver:
JSAny
(this
в JavaScript) - target:
JSFunction
(arguments.callee
в JavaScript) - newTarget:
JSAny
(new.target
в 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
. Это связано с тем, что builtin
в 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;
}
Вот ещё один пример, где случай элементов словаря помечен как deferred для улучшения генерации кода для более вероятных случаев (из реализации 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);
// <и т.д.>...
Перенос кода CSA на Torque
Патч, который перенёс Array.of
служит минимальным примером переноса кода CSA на Torque.