Обзор возможностей TypeScript Native (AOT) Compiler

image-loader.svg

Абстракт

Данная статья является продолжением предыдущей статьи о TypeScript Native (AOT) Compiler(далее tsc) https://habr.com/ru/post/581308/. В данной статье мне хотелось бы остановиться на обзоре возможностей компилятора, а также использование компилятора из command line (коммандной строки). При написании примеров я буду подразумевать что у читателя есть минимальное понимание как пользоваться компиляторами в командной строке и наличие некоторого опыта работы с LLVM

Использование tsc

Для получения краткой справки об опциях программы нужно выполнить следующую команду

QC:\dev>tsc.exe --help  

Основана опция, которая нам потребуется это »--emit=». Эта опция важна для генерировал файлов в разных форматах вывода, таких как «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) -> 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;
}

В первом вариантепараметры »и «z» указаны как параметры со значением, а во втором параметры »и «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)

Если Вам понравился данный проект, то дайте мне знать какую информацию Вы бы хотели еще узнать.

© Habrahabr.ru