Имплементация нативного Ahead-of-Time компилятора для языка TypeScript

Абстракт

Со времен появления языков программирования С и С++ прошло много времени, за это время появилось много новых языков таких как Java, C#, F#, Rust, Erlang, Python и т.д. Все новые языки привносили что-то новое в способы программирования и решения типичных задач для программирования. С распространением интернета появились такие языки как JavaScript и его преемник TypeScript. Последний, в свою очередь, стал очень популярный для Web-разработок.

Вступление

С и С++ остаются лидерами среди компиляторов для нативного кода, но со временем многие люди перешли с С++ на новые языки такие как TypeScript и уже позабыли практики применяемые для программирования на С++. Позвольте мне представить Вам попытку разработать компилятор, который бы использовал TypeScript как язык для компилирования машинного кода как это делает C или С++.

TypeScript Native (AOT) Compiler (далее tsc)

Давайте посмотрим в чем разница между кодом С/C++ и таким же кодом на TypeScript.

C/C++ код

#include <cstdio>

int foo(int val)
{
  return val;
}

int main()
{
  int val = 1;
  printf("Value: %d", foo(val));
  return 0;
}	

и аналог на TypeScript

function foo(val = 0)
{
  return val;
}

function main() {
  const val = 1;
  print("Value:", foo(val));
}

Как мы видим большой разницы между кодом нет, но для любителей TypeScript синтаксис С/C++ имеет много ненужных вещей. Например, зачем явно указывать тип переменной если ее тип компилятор может определить сам во время компиляции.

tsc позволяет компилировать нативный код или выполнять сразу без компиляции (JIT). Это удобно для проверки кода без "долгой" компиляции и когда код готов, сгенерировать выполняемый файл (EXE). Так же есть возможность генерировать WASM и выполнять его в Chrome и всех броузерах, которые поддерживают WASM.

tsc Так же позволяет компилировать код с использованием GC(Garbage collection) или отключать его и управлять памятью самостоятельно.

Взаимодействие с С/C++

Так как tsc компилирует нативный код, то взаимодействие с С/C++ уже доступно "out-of-box", достаточно продекларировать функцию, которую хотите использовать, например:

type int64 = 2147483648;
declare malloc(size: int64): any;

Заключение

tsc разрабатывается с использование LLVM и находиться сейчас в активной разработке. Принять участие в разработке или попробовать компилятор самому можно на Git ASDAlexander77/TypeScriptCompiler: TypeScript Compiler (by LLVM) (github.com)

Комментарии (25)


  1. gudvinr
    04.10.2021 01:51
    +5

    Например, зачем явно указывать тип переменной если ее тип компилятор может определить сам во время компиляции.

    #include <cstdio>
    
    template <typename T>
    T foo(T val)
    {
      return val;
    }
    
    int main()
    {
      auto val = 1;
      printf("Value: %d", foo(val));
      return 0;
    }   

    Или даже так, если что-то поновее:


    #include <cstdio>
    
    auto foo(auto val)
    {
      return val;
    }
    
    int main()
    {
      auto val = 1;
      printf("Value: %d", foo(val));
      return 0;
    }   

    Но это довольно непрозрачный и плохой код, т.к. читателю не понятно, какой тип имеет val — unsigned или signed? Размерность 8, 32 или 64 бита? Надо знать как язык (и порой как конкретный компилятор для конкретной платформы) эти типы выводит, что особенно ужасно, если это код, который могут смотреть новички или люди, которые знакомы не конкретно с этим, а с другими языками.
    Поэтому преимуществом это если и можно назвать, то очень не всегда.


    1. tony-space
      04.10.2021 02:44
      -4

      Меня тоже удивило, что автор статьи этим утверждением обесценил строгую систему типов. Не стоит забывать, что компилятор делает проверки и показывает предупреждения в случае несоответствия типов аргумента:

      void foo (unsigned x=0);
      int y = 42;
      foo(y); //warning: signed/unsigned mismatch


      1. Reformat
        04.10.2021 04:04
        +8

        автор статьи этим утверждением обесценил строгую систему типов

        Автоматический вывод типов не имеет к строгости системы никакого отношения.
        В Haskell к примеру точно также можно нигде тип не указывать явно, и менее строгим он от этого не становится, тип все тот же, как если указать его явно.

        Автовывод типов/явная типизация, строгая/слабая типизация, статическая/динамическая типизация, всё это три разные вещи которые постоянно между собой путают.


        1. 0xd34df00d
          04.10.2021 04:43
          +1

          Автоматический вывод типов не имеет к строгости системы никакого отношения.
          В Haskell к примеру точно также можно нигде тип не указывать явно, и менее строгим он от этого не становится, тип все тот же, как если указать его явно.

          Имеет прямое отношение к выразительной силе системы типов (вывод типов неразрешим для достаточно мощных систем типов, ЕМНИП для кусочка System F мощнее rank-2 уже неразрешимо, даже завтипов не нужно), а я бы отождествил строгость и выразительную силу.


          Поэтому да, в Haskell98 вам аннотации не нужны, а в идрисе или агде без них никуда. Или даже в том же хаскеле, если его начать обмазывать TypeFamilies, RankNTypes и так далее.


  1. evtn
    04.10.2021 04:53
    +6

    Сама по себе тема вполне интересная, но есть два "но":
    1. Автору стоит поработать над изложением (вычитывать текст, проверять орфографию). Даже если закрыть глаза на орфографию, то в некоторых местах сломана логика повестования, например вот тут конец предложения словно вырван из другого места:

    Это удобно для проверки кода без "долгой" компиляции и когда код готов, сгенерировать выполняемый файл (EXE)

    2. Опущено гигантское количество внутренних деталей — то, что на Хабре бы очень оценили. Сейчас пост очень короткий и малоинформативный. Было бы интересно узнать, например, с какими проблемами автор столкнулся при генерации кода, насколько готов проект, какие подводные камни (например, насколько и как именно проект отступает от спецификаций). Можно было написать про стандартную библиотеку, которая уже существует и/или планируется.

    В конце концов, TypeScript это лишь очень продвинутый тайпчекер, и поставить его на "рельсы" самостоятельного исполнения (без движка JS) это очень большая задача, о которой можно много рассказать.


    1. AlexD_77 Автор
      04.10.2021 11:28
      +2

      Это моя пробная статья, когда будет время напишу больше уже о самой разработке и ее прогрессе, но приходиться выдирать время для писания статьи


      1. debagger
        04.10.2021 15:56
        +2

        Будет здорово, если напишете! Хабр, это одно из немногих мест в рунете, где аудитория любит и ценит лонгриды.


  1. Suvitruf
    04.10.2021 09:49

    но для любителей TypeScript синтаксис С/C++ имеет много ненужных вещей. Например, зачем явно указывать тип переменной если ее тип компилятор может определить сам во время компиляции.
    Эм, люди выбирают TypeScript вместо JS как раз (частично) потому что там типы есть, а вы их наоборот не указываете ????


    1. AlexD_77 Автор
      04.10.2021 13:31
      +4

      авто-определение типа — это не тоже самое что "не указываю". Вот в примере "foo(val = 0)" "= 0" говорит о то, что "val" будет целым 32битным числом со знаком (т.к. это тип целого по умолчанию). Поэтому писать код "type int = 0; foo(val: int = 0) {}" долго и не удобно. и дает одинаковый результат для проверки и компиляции


      1. Suvitruf
        04.10.2021 13:44

        Только потому что вы задали дефолтное значение.
        Сишное

        int foo(int val)

        Не эквивалентно
        function foo(val = 0)


        1. AlexD_77 Автор
          04.10.2021 17:04
          +3

          это если думать о ECMAScript то да. Но tsc компилятор не пытается повторить поведение ECMAScript а использует синтаксис TypeScript-а для компилирования нативного кода. т.е. val будет получать только "int" значения. если хочется передавать любые значения тогда надо писать "function foo(val: any = 0)"


      1. gudvinr
        04.10.2021 16:29
        +1

        Ваш пример как раз ни о чём не говорит в отрыве от конкретной имплементации. Если не смотреть в документацию, то откуда вы знаете, что там будет именно целое 32 битное число?


        Ноль не обязательно в знаковом типе хранить, поэтому там вполне можно использовать и unsigned int.


        В C, к примеру, тип int по стандарту должен содержать "at least" 16 бит, т.е. там не факт что и 32 даже будет даже если это действительно int. А если сделать жадный компилятор, который выбирает типы на основе возможных интервалов значений, то ноль можно и вовсе в 1 байт поместить, чтобы лишние 3 байта не тратить.


        В Lua все числа представляются через double, поэтому число там не целое будет.
        В JavaScript, по-моему, Number — это тоже double. Учитывая то, что взрослый TS транспайлится как раз в JS, тогда тут ещё вопрос возникает, откуда там взялись целые 32 битные числа.


        Такие угадайки разве удобнее для читателя кода будут, чем написать сразу какой вы там тип хотите?


        1. AlexD_77 Автор
          04.10.2021 17:09

          вы все верно сказали. но 32 бита для "int" используется автоматически из-за "выравнивания" и поэтому что бы использовать int16 нужно указать свой тип например: type int16 = 65535; и когда компилятор будет использовать 16 бит а не 32. Это некоторые особенности конкретной имплементации TSC и да это все нужно описывать в документации


  1. AxisPod
    04.10.2021 18:09
    +3

    Хм, уже название исполняемого файла спорное. Точно такое же название и у компилятора в js.


    1. AlexD_77 Автор
      04.10.2021 20:55

      Я знаю, но на ум ничего не пришло.


      1. indestructable
        19.10.2021 14:50

        tscc - typescript c compiler


        1. AlexD_77 Автор
          20.10.2021 13:10

          tscc - ага это как намек на С++


  1. insecto
    24.10.2021 17:59

    Судя по всему, большая часть компилятора написана oduzhar. Можно поинтересоваться, чей же это проект на самом деле? Кто в действительности этот mlir-гуру который затащил ажно целый typescript?


    1. AlexD_77 Автор
      24.11.2021 04:12

      Это один и тот же человек просто так получилось, что в одном броузере был один аккаунт, а в командной строке другой. Проект еще не закончен. там много еще чего делать, например - темплейты, юнионы, оптимизация для GC. ref. counter стратегия и так далее. работы на много лет. нужна помощь а ее нет.


      1. insecto
        24.11.2021 04:59

        Ну, похоже что помощи ждать неоткуда. Mlir, по всей видимости, новая сложная штука. Кроме того, неочевиден business value такого компилятора.


        1. AlexD_77 Автор
          24.11.2021 16:05

          иметь нормальный С++ компилятор в виде TypeScript-а .. все новички, которые хотят писать нативный код обзавидуются :)


        1. AlexD_77 Автор
          24.11.2021 16:06

          mlir часть уже как бы закончена там вряд ли будут изменения. нужна уже runtime поддержка


  1. insecto
    25.10.2021 05:24
    +1

    Ой, я понял, это две учётки одного человека. Александр, это очень круто, огромная качественная работа.


    1. AlexD_77 Автор
      24.11.2021 04:14

      спасибо.. век помнить буду :)


  1. victorZX
    24.11.2021 04:20
    +1

    Автору спасибо за перевод. Интересный проект. На гитхабе всего два мейнтейнера, надеюсь, ребята не обломаются и не забросят проект. Тайпскрипт так и просится, чтобы у него были другие тартеты компиляции, а не только js. Видел где-то на гитхабе попытки компилить ts в php, но все в рамках эксперимента, не более. Есть проект assemblyScript, тоже пока больше похоже на эксперимент, но там больше мейнтейнеров. Ну что ж, понаблюдаем за коммиттерами, может выйдет что-то годное