Внимательные разработчики на 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, а ещё за объяснениями запланированной мной архитектуры.
Читайте также:
- 382 часа на изучение Rust и блестящая обезьянка
- Обработка ошибок на Rust: безопасный и чистый код без unwrap
- Многопоточность на Rust: ускоряем приложения, делаем их эффективнее
Читайте нас в Telegram, VK и Дзен
Перевод статьи John Nunley: Why am I writing a Rust compiler in C?





