Компиляторы
Семантические проходы и lowering
Rustc проходит по коду пять раз прежде чем сгенерировать машинный код: парсинг, desugaring, type checking, borrow checking, codegen. Каждый проход упрощает задачу следующего. Это как разборка сложного IKEA furniture: сначала читаем инструкцию, потом раскладываем детали, потом собираем - не пытаясь делать всё одновременно.
- **Rustc MIR** был введён в 2016 (RFC 1211) и позволил реализовать NLL (Non-Lexical Lifetimes) - более точный borrow checker, который принял миллионы ранее отвергаемых корректных программ.
- **TypeScript** десугарит `async/await` в state machine при компиляции в ES5 - миллионы строк асинхронного кода работают на браузерах без поддержки async, потому что компилятор делает это автоматически.
- **C++20 constexpr** позволяет вычислять lookup tables при компиляции: хеш-таблицы, таблицы тригонометрии, CRC-таблицы. LLVM сам активно использует это для встроенных таблиц архитектурных инструкций.
Desugaring
Desugaring - преобразование синтаксического сахара в более простые конструкции языка. Компилятор сначала парсит высокоуровневый синтаксис, затем раскрывает его в эквивалентные примитивные конструкции. После этого все последующие фазы работают только с ядром языка.
Kotlin desugars operator overloading: `a + b` -> `a.plus(b)`, `a[i]` -> `a.get(i)`, `a in b` -> `b.contains(a)`. Scala desugars for-comprehensions в `flatMap`/`map`/`filter`. TypeScript desugars `async/await` в state machine на Promise. Это позволяет добавлять синтаксический сахар без усложнения core языка и backend компилятора.
В чём главное преимущество desugaring для архитектуры компилятора?
Lowering
Lowering - снижение уровня абстракции: перевод высокоуровневого AST в более низкоуровневое промежуточное представление (IR). В отличие от desugaring (AST -> AST одного языка), lowering переходит к другому IR: Rustc AST -> HIR -> MIR -> LLVM IR.
MIR в Rustc был введён в 2016 году (RFC 1211) и радикально упростил borrow checker: вместо анализа сложного AST borrow checker работает с простым CFG. MIR также открыл путь к `const fn` выполнению при компиляции - MIRI (MIR interpreter) вычисляет константные выражения прямо на MIR.
Почему введение MIR в Rust упростило borrow checker?
AST Annotation
AST annotation - процесс добавления дополнительной информации к узлам AST после semantic analysis. После type checking каждый узел выражения несёт свой тип; после name resolution - DefId. Аннотированный AST служит входом для IR generation.
Clang хранит всю семантическую информацию в ASTContext - единственном объекте, живущем всё время компиляции. Это включает типы, implicit conversions, template instantiations. Размер ASTContext для крупного C++ проекта может достигать нескольких гигабайт в памяти - поэтому Unity Build (объединение .cpp файлов) существенно ускоряет компиляцию, переиспользуя один ASTContext.
Что хранится в аннотированном AST после type checking?
Constant Evaluation
Constant evaluation (const eval) - вычисление значений во время компиляции. Включает: размеры массивов, enum discriminants, `constexpr` в C++, `const fn` в Rust, `inline` и `readonly` в TypeScript. Позволяет переносить вычисления из runtime в compile time.
C++ комитет продолжает расширять `constexpr`: в C++20 разрешено использование `std::vector` и `std::string` в constexpr контексте - значения аллоцируются и вычисляются при компиляции, потом результат встраивается в константный массив. GCC и Clang вычисляют constexpr через отдельный interpreter, Rust использует MIRI. Это создаёт интересный эффект: сложные вычисления могут значительно замедлить компиляцию.
Чем `consteval` в C++20 отличается от `constexpr`?
Ключевые идеи
- **Desugaring** раскрывает синтаксический сахар в примитивные конструкции. `for`, `?`, `async/await` - всё это сахар поверх loop/match/closure.
- **Lowering** снижает уровень абстракции: AST -> HIR -> MIR -> LLVM IR в Rustc. Каждый уровень проще и ближе к машине.
- **AST annotation** добавляет типы и DefIds к узлам после semantic analysis. Аннотированный AST - полная информация для codegen.
- **Constant evaluation** вычисляет значения при компиляции через interpreter (MIRI в Rust, встроенный interpreter в GCC/Clang для constexpr).
Связанные темы
Семантические проходы соединяют frontend с backend компилятора:
- Промежуточное представление (IR) — Lowering производит IR - входные данные для оптимизаций и codegen
- Проверка типов — Type checking предшествует desugaring в некоторых компиляторах (Rustc: HIR level) и следует в других
- Семантические ошибки — Большинство семантических ошибок фиксируются в ходе semantic passes до lowering
Вопросы для размышления
- Rustc проходит по коду пять раз (AST, HIR, MIR, LLVM IR, asm). Почему не объединить все фазы в один проход - это ускорило бы компиляцию?
- TypeScript десугарит `async/await` в state machine при таргете ES5, но не при ES2017+. Как компилятор решает, какой уровень desugaring применять?
- Constant evaluation в C++ позволяет запускать произвольный код во время компиляции. Какие риски это создаёт - и как компиляторы их ограничивают?