Обзор возможностей TypeScript Native (AOT) Compiler
Абстракт
Данная статья является продолжением предыдущей статьи о TypeScript Native (AOT) Compiler(далее tsc) https://habr.com/ru/post/581308/. В данной статье мне хотелось бы остановиться на обзоре возможностей компилятора, а также использование компилятора из command line (коммандной строки). При написании примеров я буду подразумевать что у читателя есть минимальное понимание как пользоваться компиляторами в командной строке и наличие некоторого опыта работы с LLVM
Использование tsc
Для получения краткой справки об опциях программы нужно выполнить следующую команду
QC:\dev>tsc.exe --help
Основана опция, которая нам потребуется это »--emit=
Генерирование OBJ (объектный файл)
Предположим, у нас есть файл example1.ts который содержит простую программу
function main()
{
print("Hello World!");
}
и мы хотим получить obj. файл, то достаточно выполнить следующую команду
C:\dev>tsc.exe --emit=jit -nogc -dump-object-file -object-filename=out.o example.ts
Данная команда скомпилирует файл example.ts и сохранит OBJ в out.o. Далее сгенерированный файл можно использовать для получения исполняемого файла. Например, используя lld
Генерирование EXE (выполняемого файла)
Берем out.o из предыдущего примера и выполняем следующую команду
c:\LLVM\lld.exe -flavor link out.o /libpath:%LIBPATH% /libpath:%SDKPATH% /libpath:%UCRTPATH% msvcrt.lib ucrt.lib kernel32.lib user32.lib.lib
Альтернативный путь генерирования EXE (выполняемого файла)
C:\dev>tsc.exe --emit=llvm example1.ts 2>example1.ll
C:\LLVM\llc.exe --filetype=obj -o=out.o example1.ll
C:\LLVM\lld.exe -flavor link out.o
когда будете выполнять последний шаг то может понадобиться указать нужные библиотеки для C чтобы скомпилировать в .Exe файл (в конце статьи я приведу свои batch файлы для более детальной информации по компиляции)
Выполнение TypeScript кода без компиляции
Если нам нужно только посмотреть результат работы программы, то достаточно ввести следующую команду
C:\dev>tsc.exe --jit example1.ts
Шаги компилятора для генерирования исполняемого кода
После того как мы ознакомились с базовыми командами программы для командной строки, предлагаю посмотреть, как tsc компилирует программу example1.ts
C:\dev>tsc.exe --emit=mlir example1.ts
После чего мы получим вывод в MLIR (больше о MLIR тут https://mlir.llvm.org/)
module @"1.ts" {
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%0 = ts.UnresolvedSymbolRef {identifier = @print} : none
%1 = ts.Constant {value = "Hello World!"} : !ts.string
ts.Print(%1) : !ts.string
"ts.Exit"() : () -> ()
}
}
TypeScript файл будет преобразован в псевдоязык, который описывает логику программы. Этот промежуточный шаг позволяет компилятору проводить анализ кода и выполнять некоторую оптимизацию на данном этапе.
Что бы получить обработанной код нужно ввести следующую команду
C:\dev>tsc.exe --emit=mlir-affine example1.ts
Мы получим следующий листинг.
module @"example1.ts" {
ts.Func @main () -> () {
%0 = ts.Constant {value = "Hello World!"} : !ts.string
ts.Print(%0) : !ts.string
ts.ReturnInternal
}
}
Что нам говорит следующий псевдокод, что есть программа »example1.ts» которая имеет функцию »@main» которая в свою очередь загружает константу в параметр псевдокоманды «Print» и выходит из функции. Как не трудно догадаться, что при выполнении данной программы, мы увидим нашу строку вывода «Hello World!».
Дальнейшим шагом хотелось бы посмотреть выполняемый код близкий к машинному коду. Для этого мы выполним следующую команду
C:\dev>tsc.exe --emit=mlir-llvm example1.ts
И получаем результат выполнения
module @"1.ts" {
llvm.func @puts(!llvm.ptr) -> i32
llvm.mlir.global internal constant @s_10092224619179044402("Hello World!\00")
llvm.func @main() {
%0 = llvm.mlir.addressof @s_10092224619179044402 : !llvm.ptr>
%1 = llvm.mlir.constant(0 : i64) : i64
%2 = llvm.getelementptr %0[%1, %1] : (!llvm.ptr>, i64, i64) -> !llvm.ptr
%3 = llvm.call @puts(%2) : (!llvm.ptr) -> i32
llvm.return
}
}
Но этого еще мало, чтобы получить файл, который мы может «скормить» llс для получения obj-файла нам нужно выполнить команду
C:\dev>tsc.exe --emit=llvm example1.ts
Получаем вывод
; ModuleID = 'LLVMDialectModule'
source_filename = "LLVMDialectModule"
target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-windows-msvc"
@s_10092224619179044402 = internal constant [13 x i8] c"Hello World!\00"
declare i32 @puts(i8*)
define void @main() {
%1 = call i32 @puts(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @s_10092224619179044402, i64 0, i64 0)), !d
ret void
}
Только что мы прошлись по основным этапам компиляции программы. Хотелось бы добавить, что шаги по оптимизации обычно выполняются на этапе, когда мы выполнили команду с параметром »--emit=mlir-affine»
Далее давайте посмотрим какой код может компилировать программа (я выбрал только более-менее интересные примеры)
Параметры функций по умолчанию
В следующем куске кода мы посмотрим, как можно использовать параметры по умолчанию
function defaultArgs(x: number, y = 3, z = 7) {
return x + y + z;
}
И вариант, когда значение по умолчанию указывать не нужно
function optargs(x: number, y?: number, z?: number) {
if (y == undefined) y = 0;
return x + y;
}
В первом вариантепараметры »y» и «z» указаны как параметры со значением, а во втором параметры »y» и «z» указаны как опциональные параметры. Заметьте, что в первом случае мы не указывает тип параметра т.к. тип параметра определяется из значения по умолчанию.
Вызов данных функций будет выглядеть следующим образом
defaultArgs(1);
defaultArgs(1, 4);
defaultArgs(1, 4, 8);
optargs(1);
optargs(1, 2);
optargs(1, 2, 3);
Полный пример кода (обратите внимание что декларировать функции можно в любом порядке)
function defaultArgs(x: number, y = 3, z = 7) {
return x + y + z;
}
function main() {
defaultArgs(1);
defaultArgs(1, 4);
defaultArgs(1, 4, 8);
optargs(1);
optargs(1, 2);
optargs(1, 2, 3);
}
function optargs(x: number, y?: number, z?: number) {
if (y == undefined) y = 0;
return x + y;
}
И получаемый псевдокод, из которого будет сгенерирован выполняемый файл (для этого нужно выполнить команду tsc --emit=mlir example1.ts)
module @"C:\\temp\\1.ts" {
ts.Func @defaultArgs (!ts.number, !ts.optional, !ts.optional) -> !ts.number {sym_visibility = "private"} {
^bb0(%arg0: !ts.number, %arg1: !ts.optional, %arg2: !ts.optional): // no predecessors
%0 = "ts.Entry"() : () -> !ts.ref
%1 = "ts.Param"(%arg0) : (!ts.number) -> !ts.ref
%2 = "ts.ParamOpt"(%arg1) ( {
%11 = ts.Constant {value = 3 : i32} : i32
ts.DefaultValue %11 : i32
}) : (!ts.optional) -> !ts.ref
%3 = "ts.ParamOpt"(%arg2) ( {
%11 = ts.Constant {value = 7 : i32} : i32
ts.DefaultValue %11 : i32
}) : (!ts.optional) -> !ts.ref
%4 = ts.Load(%1) : !ts.ref -> !ts.number
%5 = ts.Load(%2) : !ts.ref -> i32
%6 = ts.Cast %5 : i32 to !ts.number
%7 = ts.ArithmeticBinary %4(39) %6 : !ts.number, !ts.number -> !ts.number
%8 = ts.Load(%3) : !ts.ref -> i32
%9 = ts.Cast %8 : i32 to !ts.number
%10 = ts.ArithmeticBinary %7(39) %9 : !ts.number, !ts.number -> !ts.number
"ts.ReturnVal"(%10, %0) : (!ts.number, !ts.ref) -> ()
"ts.Exit"() : () -> ()
}
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%0 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%1 = ts.Constant {value = 1 : i32} : i32
%2 = ts.Cast %1 : i32 to !ts.number
%3 = ts.Undef : !ts.optional
%4 = ts.Undef : !ts.optional
%5 = ts.CallIndirect %0(%2, %3, %4) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%6 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%7 = ts.Constant {value = 1 : i32} : i32
%8 = ts.Cast %7 : i32 to !ts.number
%9 = ts.Constant {value = 4 : i32} : i32
%10 = ts.Cast %9 : i32 to !ts.optional
%11 = ts.Undef : !ts.optional
%12 = ts.CallIndirect %6(%8, %10, %11) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%13 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%14 = ts.Constant {value = 1 : i32} : i32
%15 = ts.Cast %14 : i32 to !ts.number
%16 = ts.Constant {value = 4 : i32} : i32
%17 = ts.Cast %16 : i32 to !ts.optional
%18 = ts.Constant {value = 8 : i32} : i32
%19 = ts.Cast %18 : i32 to !ts.optional
%20 = ts.CallIndirect %13(%15, %17, %19) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%21 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%22 = ts.Constant {value = 1 : i32} : i32
%23 = ts.Cast %22 : i32 to !ts.number
%24 = ts.Undef : !ts.optional
%25 = ts.Undef : !ts.optional
%26 = ts.CallIndirect %21(%23, %24, %25) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%27 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%28 = ts.Constant {value = 1 : i32} : i32
%29 = ts.Cast %28 : i32 to !ts.number
%30 = ts.Constant {value = 2 : i32} : i32
%31 = ts.Cast %30 : i32 to !ts.optional
%32 = ts.Undef : !ts.optional
%33 = ts.CallIndirect %27(%29, %31, %32) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%34 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%35 = ts.Constant {value = 1 : i32} : i32
%36 = ts.Cast %35 : i32 to !ts.number
%37 = ts.Constant {value = 2 : i32} : i32
%38 = ts.Cast %37 : i32 to !ts.optional
%39 = ts.Constant {value = 3 : i32} : i32
%40 = ts.Cast %39 : i32 to !ts.optional
%41 = ts.CallIndirect %34(%36, %38, %40) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
"ts.Exit"() : () -> ()
}
ts.Func @optargs (!ts.number, !ts.optional, !ts.optional) -> !ts.number {sym_visibility = "private"} {
^bb0(%arg0: !ts.number, %arg1: !ts.optional, %arg2: !ts.optional): // no predecessors
%0 = "ts.Entry"() : () -> !ts.ref
%1 = "ts.Param"(%arg0) : (!ts.number) -> !ts.ref
%2 = "ts.Param"(%arg1) : (!ts.optional) -> !ts.ref>
%3 = "ts.Param"(%arg2) : (!ts.optional) -> !ts.ref>
%4 = ts.Load(%2) : !ts.ref> -> !ts.optional
%5 = ts.Undef : !ts.optional
%6 = ts.LogicalBinary %4(34) %5 : !ts.optional, !ts.optional -> !ts.boolean
"ts.If"(%6) ( {
%11 = ts.Load(%2) : !ts.ref> -> !ts.optional
%12 = ts.Constant {value = 0 : i32} : i32
%13 = ts.Cast %12 : i32 to !ts.optional
ts.Store %13, %2 : !ts.optional -> !ts.ref>
"ts.Result"() : () -> ()
}, {
}) : (!ts.boolean) -> ()
%7 = ts.Load(%1) : !ts.ref -> !ts.number
%8 = ts.Load(%2) : !ts.ref> -> !ts.optional
%9 = ts.Cast %8 : !ts.optional to !ts.number
%10 = ts.ArithmeticBinary %7(39) %9 : !ts.number, !ts.number -> !ts.number
"ts.ReturnVal"(%10, %0) : (!ts.number, !ts.ref) -> ()
"ts.Exit"() : () -> ()
}
}
Данный код содержит части, которые могут быть упрощены на стадии оптимизации. Давайте посмотрим, что получиться на стадии оптимизации (для этого нам нужно выполнить команду tsc --emit=mlir-affine example1.ts)
module @"C:\\temp\\1.ts" {
ts.Func @defaultArgs (!ts.number, !ts.optional, !ts.optional) -> !ts.number {sym_visibility = "private"} {
^bb0(%arg0: !ts.number, %arg1: !ts.optional, %arg2: !ts.optional): // no predecessors
%c7_i32 = constant 7 : i32
%c3_i32 = constant 3 : i32
%0 = ts.Variable() {false} : -> !ts.ref
%1 = ts.Variable(%arg0) {false} : !ts.number -> !ts.ref
%2 = "ts.HasValue"(%arg1) : (!ts.optional) -> !ts.boolean
%3 = ts.Cast %2 : !ts.boolean to i1
cond_br %3, ^bb1, ^bb2(%c3_i32 : i32)
^bb1: // pred: ^bb0
%4 = "ts.Value"(%arg1) : (!ts.optional) -> i32
br ^bb2(%4 : i32)
^bb2(%5: i32): // 2 preds: ^bb0, ^bb1
%6 = ts.Variable(%5) {false} : i32 -> !ts.ref
%7 = "ts.HasValue"(%arg2) : (!ts.optional) -> !ts.boolean
%8 = ts.Cast %7 : !ts.boolean to i1
cond_br %8, ^bb3, ^bb4(%c7_i32 : i32)
^bb3: // pred: ^bb2
%9 = "ts.Value"(%arg2) : (!ts.optional) -> i32
br ^bb4(%9 : i32)
^bb4(%10: i32): // 2 preds: ^bb2, ^bb3
%11 = ts.Variable(%10) {false} : i32 -> !ts.ref
%12 = ts.Load(%1) : !ts.ref -> !ts.number
%13 = ts.Load(%6) : !ts.ref -> i32
%14 = ts.Cast %13 : i32 to !ts.number
%15 = ts.ArithmeticBinary %12(39) %14 : !ts.number, !ts.number -> !ts.number
%16 = ts.Load(%11) : !ts.ref -> i32
%17 = ts.Cast %16 : i32 to !ts.number
%18 = ts.ArithmeticBinary %15(39) %17 : !ts.number, !ts.number -> !ts.number
ts.Store %18, %0 : !ts.number -> !ts.ref
%19 = ts.Load(%0) : !ts.ref -> !ts.number
ts.ReturnInternal %19 : !ts.number
}
ts.Func @main () -> () {
%c3_i32 = constant 3 : i32
%c2_i32 = constant 2 : i32
%c8_i32 = constant 8 : i32
%c4_i32 = constant 4 : i32
%c1_i32 = constant 1 : i32
%0 = ts.Cast %c1_i32 : i32 to !ts.number
%1 = ts.Undef : !ts.optional
%2 = ts.Undef : !ts.optional
%3 = ts.CallInternal @defaultArgs(%0, %1, %2) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%4 = ts.Cast %c4_i32 : i32 to !ts.optional
%5 = ts.Undef : !ts.optional
%6 = ts.CallInternal @defaultArgs(%0, %4, %5) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%7 = ts.Cast %c8_i32 : i32 to !ts.optional
%8 = ts.CallInternal @defaultArgs(%0, %4, %7) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%9 = ts.Undef : !ts.optional
%10 = ts.Undef : !ts.optional
%11 = ts.CallInternal @optargs(%0, %9, %10) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%12 = ts.Cast %c2_i32 : i32 to !ts.optional
%13 = ts.Undef : !ts.optional
%14 = ts.CallInternal @optargs(%0, %12, %13) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
%15 = ts.Cast %c3_i32 : i32 to !ts.optional
%16 = ts.CallInternal @optargs(%0, %12, %15) : (!ts.number, !ts.optional, !ts.optional) -> !ts.number
ts.ReturnInternal
}
ts.Func @optargs (!ts.number, !ts.optional, !ts.optional) -> !ts.number {sym_visibility = "private"} {
^bb0(%arg0: !ts.number, %arg1: !ts.optional, %arg2: !ts.optional): // no predecessors
%c0_i32 = constant 0 : i32
%0 = ts.Variable() {false} : -> !ts.ref
%1 = ts.Variable(%arg0) {false} : !ts.number -> !ts.ref
%2 = ts.Variable(%arg1) {false} : !ts.optional -> !ts.ref>
%3 = ts.Variable(%arg2) {false} : !ts.optional -> !ts.ref>
%4 = ts.Load(%2) : !ts.ref> -> !ts.optional
%5 = ts.Undef : !ts.optional
%6 = ts.LogicalBinary %4(34) %5 : !ts.optional, !ts.optional -> !ts.boolean
%7 = ts.Cast %6 : !ts.boolean to i1
cond_br %7, ^bb1, ^bb2
^bb1: // pred: ^bb0
%8 = ts.Cast %c0_i32 : i32 to !ts.optional
ts.Store %8, %2 : !ts.optional -> !ts.ref>
br ^bb2
^bb2: // 2 preds: ^bb0, ^bb1
%9 = ts.Load(%1) : !ts.ref -> !ts.number
%10 = ts.Load(%2) : !ts.ref> -> !ts.optional
%11 = ts.Cast %10 : !ts.optional to !ts.number
%12 = ts.ArithmeticBinary %9(39) %11 : !ts.number, !ts.number -> !ts.number
ts.Store %12, %0 : !ts.number -> !ts.ref
%13 = ts.Load(%0) : !ts.ref -> !ts.number
ts.ReturnInternal %13 : !ts.number
}
}
И наконец, чтобы посмотреть, как работает данный код нужно запустить команду tsc --emit=jit -nogc example1.ts
В дальнейшем все листинги приводить буду по минимуму из-за большого их объёма.
Итерация массивов
В следующем коде посмотрим, как компилятор может генерировать код для обхода массивов
function main() {
const trees = [[1], [2, 3], [4, 5, 6]];
for (const a of trees) {
print("array");
for (const b of a) {
print(b);
}
}
print("done.");
}
Результат работы (tsc --emit=jit -nogc example1.ts)
C:\>tsc.exe --emit=jit -nogc example1.ts
array
1
array
2
3
array
4
5
6
done.
Давайте посмотрим на сгенерированный псевдокод (tsc --emit=mlir example1.ts)
module @"C:\\temp\\1.ts" {
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%0 = ts.Constant {value = 1 : i32} : i32
%1 = ts.Constant {value = [1 : i32]} : !ts.const_array
%2 = ts.Constant {value = 2 : i32} : i32
%3 = ts.Constant {value = 3 : i32} : i32
%4 = ts.Constant {value = [2 : i32, 3 : i32]} : !ts.const_array
%5 = ts.Constant {value = 4 : i32} : i32
%6 = ts.Constant {value = 5 : i32} : i32
%7 = ts.Constant {value = 6 : i32} : i32
%8 = ts.Constant {value = [4 : i32, 5 : i32, 6 : i32]} : !ts.const_array
%9 = ts.Constant {value = [[1 : i32], [2 : i32, 3 : i32], [4 : i32, 5 : i32, 6 : i32]]} : !ts.const_array,3>
%10 = ts.Constant {value = 0 : i32} : i32
%11 = ts.Variable(%10) {false} : i32 -> !ts.ref
ts.For : -> ( {
%14 = ts.Load(%11) : !ts.ref -> i32
%15 = ts.Constant {value = 3 : i32} : i32
%16 = ts.LogicalBinary %14(29) %15 : i32, i32 -> !ts.boolean
ts.Condition(%16) :
}, {
%14 = ts.Load(%11) : !ts.ref -> i32
%15 = ts.PrefixUnary %14(45) : i32 -> i32
"ts.Result"() : () -> ()
}) {
%14 = ts.Load(%11) : !ts.ref -> i32
%15 = ts.ElementRef %9[%14] : !ts.const_array,3>[i32] -> !ts.ref>
%16 = ts.Load(%15) : !ts.ref> -> !ts.array
%17 = ts.UnresolvedSymbolRef {identifier = @print} : none
%18 = ts.Constant {value = "array"} : !ts.string
ts.Print(%18) : !ts.string
%19 = ts.Constant {value = 0 : i32} : i32
%20 = ts.Variable(%19) {false} : i32 -> !ts.ref
ts.For : -> ( {
%21 = ts.Load(%20) : !ts.ref -> i32
%22 = ts.Load(%15) : !ts.ref> -> !ts.array
%23 = "ts.LengthOf"(%22) : (!ts.array) -> i32
%24 = ts.LogicalBinary %21(29) %23 : i32, i32 -> !ts.boolean
ts.Condition(%24) :
}, {
%21 = ts.Load(%20) : !ts.ref -> i32
%22 = ts.PrefixUnary %21(45) : i32 -> i32
"ts.Result"() : () -> ()
}) {
%21 = ts.Load(%20) : !ts.ref -> i32
%22 = ts.Load(%15) : !ts.ref> -> !ts.array
%23 = ts.ElementRef %22[%21] : !ts.array[i32] -> !ts.ref
%24 = ts.Load(%23) : !ts.ref -> i32
%25 = ts.UnresolvedSymbolRef {identifier = @print} : none
ts.Print(%24) : i32
"ts.Result"() : () -> ()
}
"ts.Result"() : () -> ()
}
%12 = ts.UnresolvedSymbolRef {identifier = @print} : none
%13 = ts.Constant {value = "done."} : !ts.string
ts.Print(%13) : !ts.string
"ts.Exit"() : () -> ()
}
}
Захват переменных функциями
function func_in_object(a: number | undefined) {
const s = {
f() {
assert(a == 11);
print(a);
},
};
s.f();
}
function main() {
func_in_object(11);
print("done.");
}
Результат работы:
C:\temp>C:\tsc.exe --emit=jit -nogc example1.ts
11
done.
В данном примере, передается параметр »a» с типом »number | undefined». Это значит, что,»а» может быть опциональный т.е. аналог »a?: number». Далее этот параметр будет захвачен в функции »f ()» которая будет принадлежать абстрактному объекту »s»
И давайте посмотрим какой псевдокод будет сгенерирован в данном примере
module @"C:\\temp\\1.ts" {
ts.Func @__uf14725333033948923702 (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> () {arg_attrs = [{ts.nest}, {}], sym_visibility = "private", ts.nest} {
^bb0(%arg0: !ts.ref>}>>, %arg1: !ts.object>}>>, !ts.opaque) -> ()}>>): // no predecessors
"ts.Entry"() : () -> ()
%0 = "ts.Param"(%arg1) : (!ts.object>}>>, !ts.opaque) -> ()}>>) -> !ts.ref>}>>, !ts.opaque) -> ()}>>>
%1 = ts.PropertyRef %arg0<0> : !ts.ref>}>> -> !ts.ref>>
%2 = ts.Load(%1) : !ts.ref>> -> !ts.ref>
%3 = ts.UnresolvedSymbolRef {identifier = @assert} : none
%4 = ts.Load(%2) : !ts.ref> -> !ts.optional
%5 = ts.Constant {value = 11 : i32} : i32
%6 = ts.Cast %4 : !ts.optional to i32
%7 = ts.LogicalBinary %6(34) %5 : i32, i32 -> !ts.boolean
ts.Assert %7, "assert"
%8 = ts.UnresolvedSymbolRef {identifier = @print} : none
%9 = ts.Load(%2) : !ts.ref> -> !ts.optional
ts.Print(%9) : !ts.optional
"ts.Exit"() : () -> ()
}
ts.Func @func_in_object (!ts.optional) -> () {sym_visibility = "private"} {
^bb0(%arg0: !ts.optional): // no predecessors
"ts.Entry"() : () -> ()
%0 = "ts.Param"(%arg0) : (!ts.optional) -> !ts.ref>
%1 = ts.SymbolRef {identifier = @__uf14725333033948923702} : (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()
%2 = ts.Load(%0) : !ts.ref> -> !ts.optional
%3 = "ts.Capture"(%0) : (!ts.ref>) -> !ts.ref>}>>
%4 = "ts.Trampoline"(%1, %3) {allocInHeap = false} : ((!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> (), !ts.ref>}>>) -> ((!ts.object>}>>, !ts.opaque) -> ()}>>) -> ())
%5 = ts.Constant {value = [unit]} : !ts.const_tuple<{"f",(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>
%6 = ts.Variable(%5) {false} : !ts.const_tuple<{"f",(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}> -> !ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>>
%7 = ts.PropertyRef %6<0> : !ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.bound_ref<(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()>
%8 = ts.Load(%7) : !ts.bound_ref<(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.this_func<(!ts.ref>}>>,!ts.object>}>>, !ts.opaque) -> ()}>>) -> ()>
%9 = ts.Cast %4 : (!ts.object>}>>, !ts.opaque) -> ()}>>) -> () to (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()
ts.Store %9, %7 : (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> () -> !ts.bound_ref<(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()>
%10 = ts.Load(%6) : !ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.tuple<{"f",(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>
%11 = ts.PropertyRef %6<0> : !ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.bound_ref<(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()>
%12 = ts.Load(%11) : !ts.bound_ref<(!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.this_func<(!ts.ref>}>>,!ts.object>}>>, !ts.opaque) -> ()}>>) -> ()>
%13 = ts.GetThis %12 : !ts.this_func<(!ts.ref>}>>,!ts.object>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.ref>}>>
%14 = ts.GetMethod %12 : !ts.this_func<(!ts.ref>}>>,!ts.object>}>>, !ts.opaque) -> ()}>>) -> ()> -> (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()
%15 = ts.Undef : !ts.object>}>>, !ts.opaque) -> ()}>>
ts.CallIndirect %14(%13, %15) : (!ts.ref>}>>, !ts.object>}>>, !ts.opaque) -> ()}>>) -> ()
"ts.Exit"() : () -> ()
}
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%0 = ts.SymbolRef {identifier = @func_in_object} : (!ts.optional) -> ()
%1 = ts.Constant {value = 11 : i32} : i32
%2 = ts.Cast %1 : i32 to !ts.optional
ts.CallIndirect %0(%2) : (!ts.optional) -> ()
%3 = ts.UnresolvedSymbolRef {identifier = @print} : none
%4 = ts.Constant {value = "done."} : !ts.string
ts.Print(%4) : !ts.string
"ts.Exit"() : () -> ()
}
}
Интересующие строки, где псевдокоманды, которые нас интересуют, имеют имена »ts.Capture» и »ts.Trampoline». Первая команда захватывает ссылку на объект или значение, а вторая команда создает виртуальную функцию, которая будет получать объект, содержащий ссылку на переменную »а»
Интерфейсы по требованию
В этом параграфе мы рассмотрим пример, который покажет, как можно создавать интерфейсы из классов, которые не задекларированы на уровне класса
class S {
// нам не нужно указывать наличие интерфейса в декларации
// class S implements IPrn
print() {
print("Hello World");
}
}
interface IPrn {
print();
}
function run(iface: IPrn) {
iface.print();
}
function main() {
const s = new S();
let iface = s;
iface.print();
run(s);
print("done.");
}
В этом коде мы создаем экземпляр интерфейса »IPrn» который не объявлен в классе »S» как это обычно требуется в С++
Использование yield в генераторах
Пример использования »yield».
function* foo() {
let i = 1;
while (i < 3) {
yield ++i;
}
}
function main() {
for (const o of foo3()) {
print(o);
}
}
Результат выполнения кода
C:\>tsc.exe --emit=jit -nogc example1.ts
2
3
Примеры с использованием async/await
В этом параграфе я покажу два примера с async/await и for await
async function f(a = 1) {
return a;
}
function main() {
const v = await f(2);
print(v);
print("done.");
}
Давайте посмотрим на сгенерированный псевдокод
module @"C:\\temp\\1.ts" {
ts.Func @f (!ts.optional) -> i32 {sym_visibility = "private"} {
^bb0(%arg0: !ts.optional): // no predecessors
%0 = "ts.Entry"() : () -> !ts.ref
%1 = "ts.ParamOpt"(%arg0) ( {
%3 = ts.Constant {value = 1 : i32} : i32
ts.DefaultValue %3 : i32
}) : (!ts.optional) -> !ts.ref
%2 = ts.Load(%1) : !ts.ref -> i32
"ts.ReturnVal"(%2, %0) : (i32, !ts.ref) -> ()
"ts.Exit"() : () -> ()
}
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%token, %results = async.execute -> !async.value {
%4 = ts.SymbolRef {identifier = @f} : (!ts.optional) -> i32
%5 = ts.Constant {value = 2 : i32} : i32
%6 = ts.Cast %5 : i32 to !ts.optional
%7 = ts.CallIndirect %4(%6) : (!ts.optional) -> i32
async.yield %7 : i32
}
%0 = async.await %results : !async.value
%1 = ts.UnresolvedSymbolRef {identifier = @print} : none
ts.Print(%0) : i32
%2 = ts.UnresolvedSymbolRef {identifier = @print} : none
%3 = ts.Constant {value = "done."} : !ts.string
ts.Print(%3) : !ts.string
"ts.Exit"() : () -> ()
}
}
И второй пример с for/await
function main() {
for await (const v of [1, 2, 3, 4, 5]) {
print(v);
}
}
сгенерированный псевдокод
module @"C:\\temp\\1.ts" {
ts.Func @main () -> () {
"ts.Entry"() : () -> ()
%0 = ts.Constant {value = 1 : i32} : i32
%1 = ts.Constant {value = 2 : i32} : i32
%2 = ts.Constant {value = 3 : i32} : i32
%3 = ts.Constant {value = 4 : i32} : i32
%4 = ts.Constant {value = 5 : i32} : i32
%5 = ts.Constant {value = [1 : i32, 2 : i32, 3 : i32, 4 : i32, 5 : i32]} : !ts.const_array
%6 = ts.Constant {value = 0 : i32} : i32
%7 = ts.Variable(%6) {false} : i32 -> !ts.ref
%8 = async.create_group
ts.For : -> ( {
%9 = ts.Load(%7) : !ts.ref -> i32
%10 = ts.Constant {value = 5 : i32} : i32
%11 = ts.LogicalBinary %9(29) %10 : i32, i32 -> !ts.boolean
ts.Condition(%11) :
}, {
%9 = ts.Load(%7) : !ts.ref -> i32
%10 = ts.PrefixUnary %9(45) : i32 -> i32
"ts.Result"() : () -> ()
}) {
%9 = ts.Load(%7) : !ts.ref -> i32
%token = async.execute {
%11 = ts.ElementRef %5[%9] : !ts.const_array[i32] -> !ts.ref
%12 = ts.Load(%11) : !ts.ref -> i32
%13 = ts.UnresolvedSymbolRef {identifier = @print} : none
ts.Print(%12) : i32
async.yield
}
%10 = async.add_to_group %token, %8 : !async.token
"ts.Result"() : () -> ()
}
async.await_all %8
"ts.Exit"() : () -> ()
}
}
Интересующие нас команды в этом коде это »async.execute» которые берут всю ответственность за выполнение кода асинхронно на себя
Хочется добавить, что чтобы запустить данный код нам нужна поддержка »runtime».
C:\>tsc.exe --emit=jit --shared-libs=TypeScriptRuntime.dll example1.ts
1
2
3
4
5
Как мы видим из исполняемой строки что бы активировать »runtime» нужно указать параметр »--shared-libs=TypeScriptRuntime.dll». Для компиляции в LLVM IR данный параметр не нужен, но если мы хотим получить obj-файл с использованием --emit=jit тогда «runtime» нужно указывать. Также »runtime» нужно указывать при использовании »Garbage Collector» (сборщика мусора)
Примеры Batch-файлов для компиляции Exe-файлов
Компиляция, когда не нужна поддержка GC, async и захвата переменных.
set FILENAME=%1
set LLVMPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\bin
set TSCPATH=C:\dev\TypeScriptCompiler\__build\tsc\bin
set LIBPATH="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\lib\x64"
set SDKPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64"
set UCRTPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64"
set LLVMLIBPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\lib
%TSCPATH%\tsc.exe --emit=llvm -nogc C:\temp\%FILENAME%.ts 2>%FILENAME%.ll
%LLVMPATH%\llc.exe --filetype=obj -o=%FILENAME%.o %FILENAME%.ll
%LLVMPATH%\lld.exe -flavor link %FILENAME%.o /libpath:%LIBPATH% /libpath:%SDKPATH% /libpath:%UCRTPATH% /defaultlib:libcmt.lib libvcruntime.lib
del %FILENAME%.o
echo "RUN:..."
%FILENAME%.exe
Компиляция, когда нужна поддержка захвата переменных
set FILENAME=%1
set LLVMPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\bin
set TSCPATH=C:\dev\TypeScriptCompiler\__build\tsc\bin
set LIBPATH="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\lib\x64"
set SDKPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64"
set UCRTPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64"
set CLANGLIBPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\lib\clang\13.0.0\lib\windows
%TSCPATH%\tsc.exe --emit=llvm -nogc C:\temp\%FILENAME%.ts 2>%FILENAME%.ll
%LLVMPATH%\llc.exe --filetype=obj -o=%FILENAME%.o %FILENAME%.ll
%LLVMPATH%\lld.exe -flavor link %FILENAME%.o /libpath:%LIBPATH% /libpath:%SDKPATH% /libpath:%UCRTPATH% /libpath:%CLANGLIBPATH% /defaultlib:libcmt.lib libvcruntime.lib clang_rt.builtins-x86_64.lib
del %FILENAME%.o
echo "RUN:..."
%FILENAME%.exe
Компиляция если нужна поддержка GC
set FILENAME=%1
set LLVMPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\bin
set TSCPATH=C:\dev\TypeScriptCompiler\__build\tsc\bin
set LIBPATH="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\lib\x64"
set SDKPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64"
set UCRTPATH="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64"
set LLVMLIBPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\lib
set GCLIBPATH=C:\dev\TypeScriptCompiler\3rdParty\gc\Release
%TSCPATH%\tsc.exe --emit=llvm C:\temp\%FILENAME%.ts 2>%FILENAME%.ll
%LLVMPATH%\llc.exe --filetype=obj -o=%FILENAME%.o %FILENAME%.ll
%LLVMPATH%\lld.exe -flavor link %FILENAME%.o /libpath:%LIBPATH% /libpath:%SDKPATH% /libpath:%UCRTPATH% /libpath:%GCLIBPATH% msvcrt.lib ucrt.lib kernel32.lib user32.lib gcmt-lib.lib
del %FILENAME%.o
echo "RUN:..."
%FILENAME%.exe
Пример компиляции на Ubuntu
#/bin/sh
./tsc --emit=mlir-llvm -nogc /mnt/c/temp/1.ts -debug-only=llvm 2>1d.mlir
./tsc --emit=llvm -nogc /mnt/c/temp/1.ts 2>1.ll
llc --filetype=obj --relocation-model=pic -o=1.o 1.ll
И если нужна поддержка try/catch/finally
#/bin/sh
./tsc --emit=llvm -nogc /mnt/c/temp/1.ts 2>1.ll
llc --filetype=obj --relocation-model=pic -o=1.o 1.ll
gcc -o 1.out 1.o -frtti -fexceptions -lstdc++
Пример компиляции для WASM
set FILENAME=%1
set LLVMPATH=C:\dev\TypeScriptCompiler\3rdParty\llvm\release\bin
set TSCPATH=C:\dev\TypeScriptCompiler\__build\tsc\bin
%TSCPATH%\tsc.exe --emit=llvm C:\temp\%FILENAME%.ts 2>%FILENAME%.ll
%LLVMPATH%\llc.exe -mtriple=wasm32-unknown-unknown -O3 --filetype=obj -o=%FILENAME%.o %FILENAME%.ll
%LLVMPATH%\wasm-ld.exe %FILENAME%.o -o %FILENAME%.wasm --no-entry --export-all --allow-undefined
del *.o
Больше примеров batch-файлов можно посмотреть тут TypeScriptCompiler/docs/how at main · ASDAlexander77/TypeScriptCompiler (github.com)
Заключение
Я рекомендую заглянуть в репозиторий проекта в папку test что бы посмотреть примеры кодов, которые могут быть скомпилированы tsc.
TypeScriptCompiler/tsc/test/tester/tests at main · ASDAlexander77/TypeScriptCompiler (github.com)
Если Вам понравился данный проект, то дайте мне знать какую информацию Вы бы хотели еще узнать.