Внимательные разработчики на Rust, возможно, заметили, что в последнее время моя активность снизилась. Тому есть несколько разных причин. Я стал объектом поистине апокалиптической серии событий в жизни, включая смерть родственника. Она потрясла меня до глубины души. У меня появилось больше обязанностей на работе, и осталось меньше времени и энергии, чтобы контрибьютить. Возможно, я также немного потерял тот студенческий энтузиазм, который изначально привёл меня к открытому исходному коду.

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

Я пишу компилятор Rust. Там нет ничего, кроме чистого C. Не flex, не yacc и даже не Makefile. Он называется Dozer.

Подождите, а зачем?

Чтобы понять, почему я пошёл по этому безумному пути, сначала нужно понимать бутстрепинг и почему он важен.

Допустим, вы написали код на Rust. Чтобы запустить этот код, вам необходимо его скомпилировать. Компилятор — это программа, которая анализирует код, проверяет его правильность, а затем преобразует его в машинный код, понятный процессору.

Да, это намного сложнее. Кроме случаев, когда не так сложно. Компиляторы сложно даже описать.

Для Rust основной компилятор — rustc. Это базовая программа, которую cargo вызывает при запуске cargo build. Это фантастическое программное обеспечение и, честно говоря, жемчужина сообщества открытого исходного кода. Качество его кода не уступает ядру Linux и исходному коду Quake III.

Однако rustс сам по себе уже является программой. Поэтому ей нужен компилятор, чтобы скомпилировать её из исходного кода в машинный. Скажите, на каком языке написан rustc?

rustc — это программа на Rust. Она написана на Rust с целью компиляции кода на Rust. Но задумайтесь об этом на секунду. Если rustc написана на Rust, и rustc необходим для компиляции кода Rust, это означает, что вам нужно использовать rustc для компиляции rustc. Это нормально для нас, пользователей, ведь мы можем просто скачать rustc из интернета и воспользоваться ей.

Но какая программа скомпилировала первый rustc? Раньше яйца должна была быть курица, верно? Где компиляция начинается?

На самом деле это довольно просто. Каждая новая версия rustc компилировалась с предыдущей версией rustc. Итак, rustc версии 1.80.0 был скомпилирован с rustc версии 1.79.0., который, в свою очередь, скомпилирован с помощью rustc версии 1.78.0. И так далее, и тому подобное, вплоть до версии 0.7. С той точки компилятор был написан на OCaml. Итак, всё, что вам нужно, чтобы получить полнофункциональную программу rustc, — это компилятор OCaml.

Вот и решена проблема! Мы выяснили, как создать rust с самого начала! Всё в порядке, вернёмся к делу.

Но есть ещё одно. Чтобы всё это работало, нам по-прежнему нужна версия компилятора OCaml. Итак, на каком языке написан компилятор OCaml?

Существует проект, который может успешно скомпилировать компилятор OCaml с использованием Guile. Это один из многих вариантов Scheme, а тот — один из многих вариантов Lisp. Не говоря уже о том, что интерпретатор Guile написан на C.

Итак, это подводит нас, как и всё остальное, к языку программирования C. Мы просто компилируем его с помощью GCC, и всё работает. Так что нужно просто скомпилировать GCC, который написан с использованием… C++?!

Ладно, это немного несправедливо. GCC был написан на C до версии 5, и не так уж и мало компиляторов языка C, написанных на C. Например, рассмотрим TinyCC, который поддерживает не только компиляцию, а ещё сборку и компоновку. Но это всё равно не отвечает на наш вопрос. На каком языке был написан первый компилятор C? Ассемблер? Тогда на чём был написан первый ассемблер?

Принцип спуска

Ниже я представляю проект Bootstrappable. На мой взгляд, это один из самых интересных проектов в сообществе открытого исходного кода. По сути, это алхимия кода.

Их процесс бутстрепа Linux начинается с 512-байтового бинарного числа. Это число содержит, возможно, самый простой компилятор, который только можно представить: он принимает шестнадцатеричные цифры и выводит соответствующие необработанные байты. В качестве примера приведём часть «исходного кода», скомпилированного этим компилятором.

31 C0           # xor ax, ax
8E D8           # mov ds, ax
8E C0           # mov es, ax
8E D0           # mov ss, ax
BC 00 77        # mov sp, 0x7700
FC              # cld ; сброс флага направления
88 16 15 7C     # mov [boot_drive], dl

Обратите внимание, что всё после знака решётки, является комментарием, а все пробелы удаляются. Честно говоря, я даже не уверен, что это можно назвать языком программирования. Тем не менее технически это анализируемый, препарируемый исходный код.

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

DEFINE cmp_ebx,edx 39D3
DEFINE je 0F84
DEFINE sub_ebx, 81EB

:loop_options
    cmp_ebx,edx                         # Проверка, закончили ли мы
    je %loop_options_done               # Мы закончили
    sub_ebx, %2                         # --options

Странно думать, что ассемблерный код более высокоуровневый, чем что-либо ещё, верно?

Этого достаточно, чтобы получить доступ к самому основному подмножеству C. Затем компилируется немного более продвинутый компилятор C, написанный на этом подмножестве. Через несколько шагов можно скомпилировать TinyCC. Отсюда — загрузить yacc, базовые coreutils, Bash, autotools и, в конечном счёте, GCC и Linux. Я не отдаю должного процессу, он увлекательный. Каждый шаг расписан здесь.

В любом случае это переход от «двоичного объекта, достаточно маленького, чтобы его можно было анализировать вручную», к Linux, GCC и, по сути, всему остальному. Снова начнём с TinyCC. Сейчас Rust в этом процессе появляется очень поздно. Они используют mrustc — альтернативную реализацию Rust, написанную на C++, которая может компилировать rustc версии 1.56. Отсюда затем компилируют в современный код Rust. Основная проблема здесь заключается в том, что к моменту включения C++ в цепочку начальной загрузки сама загрузка практически завершается. Так что, если вы хотели использовать Rust до появления C++, вам не повезло.

Итак, для меня было бы очень хорошо, если бы существовал компилятор Rust с бутстрепом на C. В частности, компилятор Rust с бутстрепом TinyCC, в предположении, что в системе ещё нет инструментов, которые потенциально могут быть полезны. Это Dozer.

План

Я работал над Dozer последние два месяца, тратил своё тающее свободное время на работу на языке, который ненавижу.

Это немного несправедливо. C имеет некоторые первоклассные качества. Поистине реальность — это то, что вы о ней думаете. Просто я бы не подпускал этот код в продакшн.

Он написан без расширений, на данный момент и TinyCC, и cproc могут скомпилировать его без проблем. В качестве бекэнда я использую QBE. Кроме того, я предполагаю, что в системе нет никаких инструментов. Там просто компилятор C, какая-то очень простая реализация оболочки и ничего больше.

В этом посте я не буду подробно рассказывать об опыте написания компилятора. Но сейчас у меня готов лексер, а также значительная часть парсера. Расширение макросов и модулей — это то, что я откладываю как можно дольше, проверка типов поддерживает только i32, а генерация кода немного грубовата. Но это только начало.

Я с успехом могу скомпилировать этот код:

fn rust_main() -> i32 {
    (2 - 1) * 6 + 3
}

Итак, куда теперь? Вот мой план.

  • Медленно продвигать Dozer, пока он не сможет скомпилировать базовые примеры libc, затем libcore и rustc.
  • Для справки: я планирую скомпилировать бекэнд rustc Cranelift, полностью написанный на Rust. Поскольку предполагается, что у нас ещё нет C++, мы не можем скомпилировать LLVM.
  • Создать эквивалент cargo, который сможет использовать Dozer для компиляции пакетов Rust.
  • Узнать, какие исходники rustc генерируются автоматически, а затем удалить их. По правилам сборки Bootstrappable автоматически сгенерированный код не допускается.
  • Создать процесс, который можно использовать для компиляции rustc, а затем и cargo, чтобы позже использовать их скомпилированные версии для перекомпиляции канонических версий rustc/cargo.

Это определённо будет самый сложный проект, за который я когда-либо брался. Часть меня сомневается, что я смогу закончить его. Но знаете что? Лучше попробовать и проиграть, чем вообще никогда не попытаться. Следите за обновлениями Dozer, а ещё за объяснениями запланированной мной архитектуры.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи John Nunley: Why am I writing a Rust compiler in C?

Предыдущая статьяПО с открытым исходным кодом, которое облегчит вам жизнь
Следующая статьяРеализация функции Pull-to-refresh с помощью Compose Material 3