
Абстракт
Данная статья является продолжением предыдущей статьи о TypeScript Native (AOT) Compiler(далее tsc) https://habr.com/ru/post/581308/. В данной статье мне хотелось бы остановиться на обзоре возможностей компилятора, а также использование компилятора из command line(коммандной строки). При написании примеров я буду подразумевать что у читателя есть минимальное понимание как пользоваться компиляторами в командной строке и наличие некоторого опыта работы с LLVM
Использование tsc
Для получения краткой справки об опциях программы нужно выполнить следующую команду
QC:\dev>tsc.exe --help
Основана опция, которая нам потребуется это "--emit=<value>". Эта опция важна для генерировал файлов в разных форматах вывода, таких как "obj", LLVM IR и т.д.
Генерирование 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<i8>) -> i32 llvm.mlir.global internal constant @s_10092224619179044402("Hello World!\00") llvm.func @main() { %0 = llvm.mlir.addressof @s_10092224619179044402 : !llvm.ptr<array<13 x i8>> %1 = llvm.mlir.constant(0 : i64) : i64 %2 = llvm.getelementptr %0[%1, %1] : (!llvm.ptr<array<13 x i8>>, i64, i64) -> !llvm.ptr<i8> %3 = llvm.call @puts(%2) : (!llvm.ptr<i8>) -> 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<i32>, !ts.optional<i32>) -> !ts.number {sym_visibility = "private"} { ^bb0(%arg0: !ts.number, %arg1: !ts.optional<i32>, %arg2: !ts.optional<i32>): // no predecessors %0 = "ts.Entry"() : () -> !ts.ref<!ts.number> %1 = "ts.Param"(%arg0) : (!ts.number) -> !ts.ref<!ts.number> %2 = "ts.ParamOpt"(%arg1) ( { %11 = ts.Constant {value = 3 : i32} : i32 ts.DefaultValue %11 : i32 }) : (!ts.optional<i32>) -> !ts.ref<i32> %3 = "ts.ParamOpt"(%arg2) ( { %11 = ts.Constant {value = 7 : i32} : i32 ts.DefaultValue %11 : i32 }) : (!ts.optional<i32>) -> !ts.ref<i32> %4 = ts.Load(%1) : !ts.ref<!ts.number> -> !ts.number %5 = ts.Load(%2) : !ts.ref<i32> -> 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> -> 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.number>) -> () "ts.Exit"() : () -> () } ts.Func @main () -> () { "ts.Entry"() : () -> () %0 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %1 = ts.Constant {value = 1 : i32} : i32 %2 = ts.Cast %1 : i32 to !ts.number %3 = ts.Undef : !ts.optional<i32> %4 = ts.Undef : !ts.optional<i32> %5 = ts.CallIndirect %0(%2, %3, %4) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %6 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !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<i32> %11 = ts.Undef : !ts.optional<i32> %12 = ts.CallIndirect %6(%8, %10, %11) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %13 = ts.SymbolRef {identifier = @defaultArgs} : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !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<i32> %18 = ts.Constant {value = 8 : i32} : i32 %19 = ts.Cast %18 : i32 to !ts.optional<i32> %20 = ts.CallIndirect %13(%15, %17, %19) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %21 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number %22 = ts.Constant {value = 1 : i32} : i32 %23 = ts.Cast %22 : i32 to !ts.number %24 = ts.Undef : !ts.optional<!ts.number> %25 = ts.Undef : !ts.optional<!ts.number> %26 = ts.CallIndirect %21(%23, %24, %25) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number %27 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !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<!ts.number> %32 = ts.Undef : !ts.optional<!ts.number> %33 = ts.CallIndirect %27(%29, %31, %32) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number %34 = ts.SymbolRef {identifier = @optargs} : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !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<!ts.number> %39 = ts.Constant {value = 3 : i32} : i32 %40 = ts.Cast %39 : i32 to !ts.optional<!ts.number> %41 = ts.CallIndirect %34(%36, %38, %40) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number "ts.Exit"() : () -> () } ts.Func @optargs (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number {sym_visibility = "private"} { ^bb0(%arg0: !ts.number, %arg1: !ts.optional<!ts.number>, %arg2: !ts.optional<!ts.number>): // no predecessors %0 = "ts.Entry"() : () -> !ts.ref<!ts.number> %1 = "ts.Param"(%arg0) : (!ts.number) -> !ts.ref<!ts.number> %2 = "ts.Param"(%arg1) : (!ts.optional<!ts.number>) -> !ts.ref<!ts.optional<!ts.number>> %3 = "ts.Param"(%arg2) : (!ts.optional<!ts.number>) -> !ts.ref<!ts.optional<!ts.number>> %4 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %5 = ts.Undef : !ts.optional<!ts.undef_ph> %6 = ts.LogicalBinary %4(34) %5 : !ts.optional<!ts.number>, !ts.optional<!ts.undef_ph> -> !ts.boolean "ts.If"(%6) ( { %11 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %12 = ts.Constant {value = 0 : i32} : i32 %13 = ts.Cast %12 : i32 to !ts.optional<!ts.number> ts.Store %13, %2 : !ts.optional<!ts.number> -> !ts.ref<!ts.optional<!ts.number>> "ts.Result"() : () -> () }, { }) : (!ts.boolean) -> () %7 = ts.Load(%1) : !ts.ref<!ts.number> -> !ts.number %8 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %9 = ts.Cast %8 : !ts.optional<!ts.number> to !ts.number %10 = ts.ArithmeticBinary %7(39) %9 : !ts.number, !ts.number -> !ts.number "ts.ReturnVal"(%10, %0) : (!ts.number, !ts.ref<!ts.number>) -> () "ts.Exit"() : () -> () } }
Данный код содержит части, которые могут быть упрощены на стадии оптимизации. Давайте посмотрим, что получиться на стадии оптимизации (для этого нам нужно выполнить команду tsc --emit=mlir-affine example1.ts)
module @"C:\\temp\\1.ts" { ts.Func @defaultArgs (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number {sym_visibility = "private"} { ^bb0(%arg0: !ts.number, %arg1: !ts.optional<i32>, %arg2: !ts.optional<i32>): // no predecessors %c7_i32 = constant 7 : i32 %c3_i32 = constant 3 : i32 %0 = ts.Variable() {false} : -> !ts.ref<!ts.number> %1 = ts.Variable(%arg0) {false} : !ts.number -> !ts.ref<!ts.number> %2 = "ts.HasValue"(%arg1) : (!ts.optional<i32>) -> !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>) -> i32 br ^bb2(%4 : i32) ^bb2(%5: i32): // 2 preds: ^bb0, ^bb1 %6 = ts.Variable(%5) {false} : i32 -> !ts.ref<i32> %7 = "ts.HasValue"(%arg2) : (!ts.optional<i32>) -> !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>) -> i32 br ^bb4(%9 : i32) ^bb4(%10: i32): // 2 preds: ^bb2, ^bb3 %11 = ts.Variable(%10) {false} : i32 -> !ts.ref<i32> %12 = ts.Load(%1) : !ts.ref<!ts.number> -> !ts.number %13 = ts.Load(%6) : !ts.ref<i32> -> 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> -> 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<!ts.number> %19 = ts.Load(%0) : !ts.ref<!ts.number> -> !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<i32> %2 = ts.Undef : !ts.optional<i32> %3 = ts.CallInternal @defaultArgs(%0, %1, %2) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %4 = ts.Cast %c4_i32 : i32 to !ts.optional<i32> %5 = ts.Undef : !ts.optional<i32> %6 = ts.CallInternal @defaultArgs(%0, %4, %5) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %7 = ts.Cast %c8_i32 : i32 to !ts.optional<i32> %8 = ts.CallInternal @defaultArgs(%0, %4, %7) : (!ts.number, !ts.optional<i32>, !ts.optional<i32>) -> !ts.number %9 = ts.Undef : !ts.optional<!ts.number> %10 = ts.Undef : !ts.optional<!ts.number> %11 = ts.CallInternal @optargs(%0, %9, %10) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number %12 = ts.Cast %c2_i32 : i32 to !ts.optional<!ts.number> %13 = ts.Undef : !ts.optional<!ts.number> %14 = ts.CallInternal @optargs(%0, %12, %13) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number %15 = ts.Cast %c3_i32 : i32 to !ts.optional<!ts.number> %16 = ts.CallInternal @optargs(%0, %12, %15) : (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number ts.ReturnInternal } ts.Func @optargs (!ts.number, !ts.optional<!ts.number>, !ts.optional<!ts.number>) -> !ts.number {sym_visibility = "private"} { ^bb0(%arg0: !ts.number, %arg1: !ts.optional<!ts.number>, %arg2: !ts.optional<!ts.number>): // no predecessors %c0_i32 = constant 0 : i32 %0 = ts.Variable() {false} : -> !ts.ref<!ts.number> %1 = ts.Variable(%arg0) {false} : !ts.number -> !ts.ref<!ts.number> %2 = ts.Variable(%arg1) {false} : !ts.optional<!ts.number> -> !ts.ref<!ts.optional<!ts.number>> %3 = ts.Variable(%arg2) {false} : !ts.optional<!ts.number> -> !ts.ref<!ts.optional<!ts.number>> %4 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %5 = ts.Undef : !ts.optional<!ts.undef_ph> %6 = ts.LogicalBinary %4(34) %5 : !ts.optional<!ts.number>, !ts.optional<!ts.undef_ph> -> !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.number> ts.Store %8, %2 : !ts.optional<!ts.number> -> !ts.ref<!ts.optional<!ts.number>> br ^bb2 ^bb2: // 2 preds: ^bb0, ^bb1 %9 = ts.Load(%1) : !ts.ref<!ts.number> -> !ts.number %10 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %11 = ts.Cast %10 : !ts.optional<!ts.number> to !ts.number %12 = ts.ArithmeticBinary %9(39) %11 : !ts.number, !ts.number -> !ts.number ts.Store %12, %0 : !ts.number -> !ts.ref<!ts.number> %13 = ts.Load(%0) : !ts.ref<!ts.number> -> !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<i32,1> %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<i32,2> %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<i32,3> %9 = ts.Constant {value = [[1 : i32], [2 : i32, 3 : i32], [4 : i32, 5 : i32, 6 : i32]]} : !ts.const_array<!ts.array<i32>,3> %10 = ts.Constant {value = 0 : i32} : i32 %11 = ts.Variable(%10) {false} : i32 -> !ts.ref<i32> ts.For : -> ( { %14 = ts.Load(%11) : !ts.ref<i32> -> 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> -> i32 %15 = ts.PrefixUnary %14(45) : i32 -> i32 "ts.Result"() : () -> () }) { %14 = ts.Load(%11) : !ts.ref<i32> -> i32 %15 = ts.ElementRef %9[%14] : !ts.const_array<!ts.array<i32>,3>[i32] -> !ts.ref<!ts.array<i32>> %16 = ts.Load(%15) : !ts.ref<!ts.array<i32>> -> !ts.array<i32> %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<i32> ts.For : -> ( { %21 = ts.Load(%20) : !ts.ref<i32> -> i32 %22 = ts.Load(%15) : !ts.ref<!ts.array<i32>> -> !ts.array<i32> %23 = "ts.LengthOf"(%22) : (!ts.array<i32>) -> i32 %24 = ts.LogicalBinary %21(29) %23 : i32, i32 -> !ts.boolean ts.Condition(%24) : }, { %21 = ts.Load(%20) : !ts.ref<i32> -> i32 %22 = ts.PrefixUnary %21(45) : i32 -> i32 "ts.Result"() : () -> () }) { %21 = ts.Load(%20) : !ts.ref<i32> -> i32 %22 = ts.Load(%15) : !ts.ref<!ts.array<i32>> -> !ts.array<i32> %23 = ts.ElementRef %22[%21] : !ts.array<i32>[i32] -> !ts.ref<i32> %24 = ts.Load(%23) : !ts.ref<i32> -> 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.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () {arg_attrs = [{ts.nest}, {}], sym_visibility = "private", ts.nest} { ^bb0(%arg0: !ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, %arg1: !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>): // no predecessors "ts.Entry"() : () -> () %0 = "ts.Param"(%arg1) : (!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> !ts.ref<!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>> %1 = ts.PropertyRef %arg0<0> : !ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>> -> !ts.ref<!ts.ref<!ts.optional<!ts.number>>> %2 = ts.Load(%1) : !ts.ref<!ts.ref<!ts.optional<!ts.number>>> -> !ts.ref<!ts.optional<!ts.number>> %3 = ts.UnresolvedSymbolRef {identifier = @assert} : none %4 = ts.Load(%2) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %5 = ts.Constant {value = 11 : i32} : i32 %6 = ts.Cast %4 : !ts.optional<!ts.number> 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.number>> -> !ts.optional<!ts.number> ts.Print(%9) : !ts.optional<!ts.number> "ts.Exit"() : () -> () } ts.Func @func_in_object (!ts.optional<!ts.number>) -> () {sym_visibility = "private"} { ^bb0(%arg0: !ts.optional<!ts.number>): // no predecessors "ts.Entry"() : () -> () %0 = "ts.Param"(%arg0) : (!ts.optional<!ts.number>) -> !ts.ref<!ts.optional<!ts.number>> %1 = ts.SymbolRef {identifier = @__uf14725333033948923702} : (!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () %2 = ts.Load(%0) : !ts.ref<!ts.optional<!ts.number>> -> !ts.optional<!ts.number> %3 = "ts.Capture"(%0) : (!ts.ref<!ts.optional<!ts.number>>) -> !ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>> %4 = "ts.Trampoline"(%1, %3) {allocInHeap = false} : ((!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> (), !ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>) -> ((!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()) %5 = ts.Constant {value = [unit]} : !ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}> %6 = ts.Variable(%5) {false} : !ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}> -> !ts.ref<!ts.tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}>> %7 = ts.PropertyRef %6<0> : !ts.ref<!ts.tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.bound_ref<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> %8 = ts.Load(%7) : !ts.bound_ref<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.this_func<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>,!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> %9 = ts.Cast %4 : (!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () to (!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () ts.Store %9, %7 : (!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () -> !ts.bound_ref<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> %10 = ts.Load(%6) : !ts.ref<!ts.tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}> %11 = ts.PropertyRef %6<0> : !ts.ref<!ts.tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()}>> -> !ts.bound_ref<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> %12 = ts.Load(%11) : !ts.bound_ref<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.this_func<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>,!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> %13 = ts.GetThis %12 : !ts.this_func<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>,!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> -> !ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>> %14 = ts.GetMethod %12 : !ts.this_func<(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>,!ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> ()> -> (!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () %15 = ts.Undef : !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>> ts.CallIndirect %14(%13, %15) : (!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.object<!ts.const_tuple<{"f",(!ts.ref<!ts.tuple<{"a",!ts.ref<!ts.optional<!ts.number>>}>>, !ts.opaque) -> ()}>>) -> () "ts.Exit"() : () -> () } ts.Func @main () -> () { "ts.Entry"() : () -> () %0 = ts.SymbolRef {identifier = @func_in_object} : (!ts.optional<!ts.number>) -> () %1 = ts.Constant {value = 11 : i32} : i32 %2 = ts.Cast %1 : i32 to !ts.optional<!ts.number> ts.CallIndirect %0(%2) : (!ts.optional<!ts.number>) -> () %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 = <IPrn>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>) -> i32 {sym_visibility = "private"} { ^bb0(%arg0: !ts.optional<i32>): // no predecessors %0 = "ts.Entry"() : () -> !ts.ref<i32> %1 = "ts.ParamOpt"(%arg0) ( { %3 = ts.Constant {value = 1 : i32} : i32 ts.DefaultValue %3 : i32 }) : (!ts.optional<i32>) -> !ts.ref<i32> %2 = ts.Load(%1) : !ts.ref<i32> -> i32 "ts.ReturnVal"(%2, %0) : (i32, !ts.ref<i32>) -> () "ts.Exit"() : () -> () } ts.Func @main () -> () { "ts.Entry"() : () -> () %token, %results = async.execute -> !async.value<i32> { %4 = ts.SymbolRef {identifier = @f} : (!ts.optional<i32>) -> i32 %5 = ts.Constant {value = 2 : i32} : i32 %6 = ts.Cast %5 : i32 to !ts.optional<i32> %7 = ts.CallIndirect %4(%6) : (!ts.optional<i32>) -> i32 async.yield %7 : i32 } %0 = async.await %results : !async.value<i32> %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<i32,5> %6 = ts.Constant {value = 0 : i32} : i32 %7 = ts.Variable(%6) {false} : i32 -> !ts.ref<i32> %8 = async.create_group ts.For : -> ( { %9 = ts.Load(%7) : !ts.ref<i32> -> 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> -> i32 %10 = ts.PrefixUnary %9(45) : i32 -> i32 "ts.Result"() : () -> () }) { %9 = ts.Load(%7) : !ts.ref<i32> -> i32 %token = async.execute { %11 = ts.ElementRef %5[%9] : !ts.const_array<i32,5>[i32] -> !ts.ref<i32> %12 = ts.Load(%11) : !ts.ref<i32> -> 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)
Если Вам понравился данный проект, то дайте мне знать какую информацию Вы бы хотели еще узнать.
