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

Когда команда TypeScript решила портировать свой компилятор на новый язык, у них была четкая цель: сохранить все в точности так, как было раньше. Это означало сохранение тех же сообщений об ошибках и семантики, на которые разработчики полагались годами. По словам Андерса Хейлсберга, ведущего архитектора, полная переработка была исключена, поскольку это могло нарушить обратную совместимость. Вместо этого они выбрали портирование — и это решение подготовило почву для неожиданного выбора: перейти на Rust.
Для переноса требовался язык, способный обрабатывать сложные внутренние структуры компилятора без необходимости внесения существенных изменений. Команда быстро поняла, что сборка мусора и первоклассные замыкания являются обязательными. Go предлагает и то, и другое «из коробки», а также зрелую генерацию нативного кода и параллелизм с использованием общей памяти на всех основных платформах. Rust, с другой стороны, потребовал бы значительных ручных обходных путей, особенно для циклических структур данных компилятора.
Зачем перебарщивать с ржавчиной ради порта?
Хейлсберг объяснил, что компилятор полон указателей на родительские объекты, рекурсивных типов и символов, которые ссылаются друг на друга. Это создает циклические ссылки, что естественно для языка со сборкой мусора. Среда выполнения Go справляется с этим без проблем, позволяя команде сосредоточиться на портировании, а не на борьбе с языком. Проверка заимствований в Rust, хотя и мощна для обеспечения безопасности памяти, просто не допускает такой структуры без использования небезопасного кода или уловок с подсчетом ссылок. Это добавило бы сложности и риска без очевидной выгоды.
При сравнении двух языков команда не обнаружила существенных преимуществ Rust в генерации кода или параллельном выполнении. Генерация нативного кода в Go уже достаточно зрелая, а его горутины предоставляют простую и эффективную модель для параллельного выполнения. Производительность Rust может быть немного лучше в некоторых крайних случаях, но дополнительные усилия, необходимые для того, чтобы компилятор работал с его правилами владения, не были оправданы. Портирование должно было быть прагматичным, а не демонстрацией языковых возможностей.
Совместимость и семантика: первостепенная задача.
Основной причиной переноса на Go было сохранение идентичного поведения. Разработчики полагаются на сообщения об ошибках TypeScript для отладки своего кода, и любое изменение может нарушить их рабочий процесс. Перенеся код на Go, команда смогла повторно использовать существующую логику и структуры данных, гарантируя побайтовую совместимость выходных данных. Такой подход также снижает риск появления скрытых ошибок, которые могут возникнуть при переписывании кода.
Сборка мусора в Go стала ключевым фактором успеха. Внутренний граф узлов и ссылок компилятора сильно взаимосвязан, и ручное управление памятью было бы кошмаром. С Go команда может автоматически выделять и освобождать память, позволяя им сосредоточиться на логике компилятора. Замыкания первого класса также упростили реализацию различных проходов и преобразований, выполняемых компилятором, поскольку они могут естественным образом захватывать контекст.
Проверка заимствований в Rust: непреодолимая преграда
Проверка заимствований в Rust предназначена для предотвращения состояний гонки данных и ошибок памяти во время компиляции, но она имеет строгие правила. Структуры данных компилятора TypeScript полны циклов и разделяемых ссылок, которые проверка заимствований отклоняет, если вы не используете небезопасные блоки или Rc/RefCell. Хейлсберг отметил, что это вынудит использовать ручные обходные пути для каждой циклической структуры данных, добавляя шаблонный код и усложняя его сопровождение. Никаких преимуществ в генерации кода или параллельном выполнении, оправдывающих эти дополнительные усилия, не было.
В итоге выбор был очевиден. Go предложил оптимальный баланс простоты, производительности и совместимости. Сейчас ведётся работа над портированием, и команда уверена, что обеспечит тот же опыт использования TypeScript с более быстрым и эффективным компилятором. Для разработчиков это означает отсутствие сюрпризов — просто тот же надёжный инструмент, который они всегда использовали, работающий на более современной основе.
В целом, решение выбрать Go вместо Rust для портирования на TypeScript сводится к практическим инженерным соображениям. Необходимость сборки мусора, первоклассных замыканий и беспроблемной обработки циклических ссылок сделали Go естественным выбором. Гарантии безопасности Rust впечатляют, но они имеют свою цену, которую команда TypeScript не была готова заплатить. В результате получился порт, который сохраняет все, что разработчики любят в TypeScript, закладывая при этом основу для будущих улучшений.