RFC-018: Проектирование LLVM AOT-компилятора
Справка:
Устарело:
- Старая модель "снизу вверх автоматического DAG-анализа" — заменена моделью прямых подвыражений блока spawn из RFC-024
- Неявный вывод побочных эффектов
@IO/@Pure— заменён механизмом типов ресурсов из RFC-024- Тип отображения
Arc(T)— заменён ключевым словомrefиз RFC-009 v9
Аннотация
В данном документе описывается проектирование LLVM AOT (Ahead-of-Time) компилятора для языка YaoXiang. LLVM-бэкенд совместно с VM-бэкендом (интерпретатором) использует общую компиляцию переднего плана, формируя архитектуру с двумя бэкендами, определённую в RFC-008: VM используется для разработки и отладки, LLVM — для production-развёртывания.
Основные обязанности:
Исходный код → Передний план (общий) → IR → LLVM Codegen → .o → Линковка статической библиотеки планировщика → exeКомпилятор преобразует исходный код YaoXiang в нативный машинный код, где:
| Возможность языка | Стратегия компиляции |
|---|---|
| Обычный код | Последовательный машинный код, нулевые накладные расходы планирования |
Блоки spawn { } | Прямые подвыражения → распределение задач + синхронное ожидание (в соответствии с RFC-024) |
native("symbol") | LLVM declare external + marshalling параметров (в соответствии с RFC-026) |
.drop деструктуризация | Вставка кода RAII cleanup (в соответствии с RFC-009) |
Токены &T / &mut T | Типы нулевого размера, исчезают после компиляции |
ref T общий доступ | Fat-указатель { refcount_ptr, data_ptr }, компилятор автоматически выбирает Rc/Arc |
Связь с RFC-024: RFC-024 определяет пользовательскую семантику блока spawn (создание задач из прямых подвыражений, синхронная блокировка ожидания). В данном документе описывается, как эта семантика компилируется в машинный код.
Связь с RFC-026: RFC-026 определяет пользовательский синтаксис FFI (native(), привязка методов [0], .drop). В данном документе описывается, как FFI-вызовы генерируют LLVM IR.
Мотивация
Зачем нужен LLVM AOT-компилятор?
В настоящее время YaoXiang имеет только интерпретатор как исполнительный бэкенд:
| Проблема | Влияние |
|---|---|
| Узкое место производительности | Интерпретируемое выполнение медленнее машинного кода в 10-100x |
| Сложность развёртывания | Необходимо носить с собой интерпретатор и runtime |
| Production-среда | Интерпретатор не подходит для сценариев с высокой чувствительностью к производительности |
LLVM в модели с двумя бэкендами
RFC-008 §6 определяет архитектуру с двумя бэкендами:
┌─────────────────────┐
│ Передний план компиляции (унифицированный) │
│ Lexer → Parser │
│ → TypeCheck │
│ → анализ spawn │
│ → анализ экранирования │
└──────────┬──────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ VM-бэкенд (разработка) │ │ LLVM-бэкенд (production) │
│ IR → интерпретация │ │ IR → нативный код │
│ Пошаговая отладка │ │ Линковка статической библиотеки планировщика │
│ Быстрая итерация │ │ Вывод .exe │
└───────────────────┘ └───────────────────┘Поведение двух бэкендов полностью идентично — разница только в способе выполнения. Один и тот же исходный код, одна и та же проверка типов, один и тот же результат анализа spawn.
Предложение
1. Архитектура компилятора
LLVM-бэкенд находится на последнем этапе конвейера компиляции, принимая IR от переднего плана и генерируя нативный код:
Исходный код
→ Lexer / Parser (frontend/core/)
→ TypeCheck + анализ spawn (frontend/core/typecheck/)
→ Генерация IR (middle/core/ir_gen.rs)
→ LLVM Codegen (backends/llvm/)
├── Отображение типов: типы YaoXiang → типы LLVM IR
├── Перевод функций: инструкции IR → инструкции LLVM IR
├── Раскрытие spawn: прямые подвыражения → функции задач + вызовы планировщика
├── Раскрытие FFI: вызовы native() → declare + marshalling
└── Вставка деструкторов: конец области видимости → вызовы .drop()
→ LLVM-оптимизация + генерация целевого кода
→ Линковка статической библиотеки runtime → исполняемый файл2. Процесс компиляции
Phase 1: Передний план (общий с VM-бэкендом)
- Парсинг, проверка типов, анализ блоков spawn, анализ экранирования
- Вывод: IR с аннотациями типов
Phase 2: Генерация LLVM IR
- Отображение типов, объявления функций, перевод инструкций
- Вывод: LLVM Module
Phase 3: LLVM-оптимизация
- Стандартный конвейер оптимизации LLVM (O0/O1/O2/O3)
- Встраивание, свёртывание констант, удаление мёртвого кода
Phase 4: Генерация целевого кода
- LLVM TargetMachine → файлы .o
- Платформы: Linux (ELF), macOS (Mach-O), Windows (COFF)
Phase 5: Линковка
- Линковка статической библиотеки runtime (планировщик, аллокатор)
- Вывод: исполняемый файл3. Отображение типов
3.1 Отображение типов YaoXiang → LLVM IR
| Тип YaoXiang | Тип LLVM IR | Описание |
|---|---|---|
Int | i64 | 64-битный знаковый целочисленный тип по умолчанию |
Int32 | i32 | Явный 32-битный целочисленный тип (в основном для FFI) |
Float | f64 | 64-битный тип с плавающей запятой по умолчанию |
Float32 | f32 | Явный 32-битный тип (в основном для FFI) |
Bool | i1 | Булев тип |
Char | i32 | Unicode code point |
String | { i8*, i64 } | Указатель + длина в байтах |
Void | {} | Пустой тип нулевого размера |
&T | — | Токен нулевого размера, исчезает после компиляции, не создаёт никакого IR |
&mut T | — | Токен нулевого размера, исчезает после компиляции, не создаёт никакого IR |
ref T | { i64*, T* } | Fat-указатель (указатель с подсчётом ссылок + указатель на данные) |
*T | T* | Голый указатель |
[T; N] | [N x T] | Массив фиксированной длины |
List(T) | { T*, i64, i64 } | Указатель на данные + длина + ёмкость |
| Структура | Соответствующая LLVM struct | Поля располагаются в порядке определения |
| Запись enum | { i64, [max_payload_size] } | Тег + union максимального payload |
?T | { i1, T } | Флаг наличия значения + данные (универсальное представление) |
| FFI непрозрачный тип | { i8* } | Обёртка над C-указателем |
| Указатель на функцию | T (...)* | Тип указателя на функцию |
&T/&mut Tнулевые накладные расходы во время выполнения: RFC-009 §2.7 определяет, что компилятор внутренне назначает токенам брендированные идентификаторы (уникальные compile-time целые числа). После мономорфизации и встраивания бренды полностью исчезают — в сгенерированном машинном коде нет никаких следов токенов.
3.2 Отображение типов параметров FFI
В соответствии с RFC-026 §2.2, дополнительный столбец LLVM IR:
| Тип C | Тип YaoXiang | LLVM IR | Описание |
|---|---|---|---|
int | Int32 | i32 | |
long | Int64 | i64 | |
float | Float32 | f32 | |
double | Float64 | f64 | |
char | Char | i32 | C char → YaoXiang Char (совместимость с Unicode) |
char* | String | { i8*, i64 } | marshalling: C string → YaoXiang String |
bool | Bool | i1 | |
size_t | Uint | i64 | |
void* | *Void | i8* | |
struct T* | T (прозрачный тип) | T* | Передача указателя |
typedef struct T T | T (непрозрачный тип) | { i8* } | Обёртка над C-указателем |
4. Нормализация IR и перевод инструкций
4.0 Нормализация IR (стек → регистры)
Текущий IR (src/middle/core/ir.rs) содержит инструкции работы со стеком (Push/Pop/Dup/Swap), разработанные для байткодовой VM. LLVM IR использует SSA-форму и не принимает стековые операции.
Стратегия обработки: Путь LLVM перед переводом инструкций проходит через лёгкий проход нормализации:
| Инструкция стека | Стратегия нормализации |
|---|---|
Push(r) | Записывает stack.push(r), не генерирует IR |
Pop(r) | r = stack.pop(), генерирует load (из стекового слота) |
Dup | stack.push(stack.top()), не генерирует IR |
Swap | Обмен местами двух верхних элементов стека, не генерирует IR |
После нормализации все операнды становятся ссылками на регистры/локальные переменные, все стековые операции устраняются. Этот проход выполняется первым в translator.rs.
Почему не устранить стековые инструкции на уровне IR? Потому что VM-бэкенд нуждается в семантике стека. Нормализация на входе в перевод LLVM сохраняет общий для обоих бэкендов IR — каждый бэкенд потребляет один и тот же IR согласно своим потребностям.
Предпосылка: Фаза генерации IR гарантирует баланс стека — все пути управления, достигающие одной программной точки, имеют одинаковую глубину стека (VM-байткодовый бэкенд зависит от того же предположения, иначе выполнение байткода даст сбой). Проход нормализации не проверяет эту предпосылку; при нарушении LLVM-бэкенд выдаёт неопределённое поведение.
4.1 Таблица перевода инструкций
Ниже перечислена стратегия перевода LLVM IR для каждого варианта перечисления Instruction. Имена инструкций полностью соответствуют src/middle/core/ir.rs.
Арифметические инструкции:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Add { dst, lhs, rhs } | add (целое) / fadd (float) | Выбор сложения целых или float по типу |
Sub { dst, lhs, rhs } | sub / fsub | |
Mul { dst, lhs, rhs } | mul / fmul | |
Div { dst, lhs, rhs } | sdiv / udiv / fdiv | Знаковое/беззнаковое/вещественное деление |
Mod { dst, lhs, rhs } | srem / urem | Знаковый/беззнаковый остаток |
Neg { dst, src } | sub 0, src (целое) / fneg (float) |
Побитовые инструкции:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
And { dst, lhs, rhs } | and | |
Or { dst, lhs, rhs } | or | |
Xor { dst, lhs, rhs } | xor | |
Shl { dst, lhs, rhs } | shl | Сдвиг влево |
Shr { dst, lhs, rhs } | lshr | Логический сдвиг вправо |
Sar { dst, lhs, rhs } | ashr | Арифметический сдвиг вправо |
Инструкции сравнения:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Eq { dst, lhs, rhs } | icmp eq / fcmp oeq | |
Ne { dst, lhs, rhs } | icmp ne / fcmp one | |
Lt { dst, lhs, rhs } | icmp slt / fcmp olt | |
Le { dst, lhs, rhs } | icmp sle / fcmp ole | |
Gt { dst, lhs, rhs } | icmp sgt / fcmp ogt | |
Ge { dst, lhs, rhs } | icmp sge / fcmp oge |
Инструкции управления потоком:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Jmp(label) | br label %L | Безусловный переход |
JmpIf(cond, label) | br i1 %cond, label %L, label %fallthrough | Условный переход |
JmpIfNot(cond, label) | br i1 %cond, label %fallthrough, label %L | Условный непереход |
Ret(Some(v)) | ret T %v | Возврат с значением |
Ret(None) | ret void | Возврат без значения |
Инструкции вызова:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Call { dst, func, args } | %r = call T @func(...) | Статический вызов |
CallVirt { dst, obj, method_name, args } | GEP таблицы vtable + call (указатель функции) | Виртуальный вызов метода, поиск через vtable |
CallDyn { dst, func, args } | %r = call T %func(...) | Динамический вызов (замыкания/указатели функций) |
TailCall { func, args } | musttail call / tail call | Оптимизация хвостового вызова |
Инструкции памяти:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Move { dst, src } | — | После нормализации становится копированием регистров, SSA-конструкция устраняет большинство |
Load { dst, src } | %v = load T, T* %src | |
Store { dst, src } | store T %src, T* %dst | |
Alloc { dst, size } | %p = alloca T (стек) / call @malloc (экранирование в кучу) | Анализ экранирования определяет место аллокации |
Free(ptr) | call @free(%ptr) (куча) / — (стек, автоматическое высвобождение) | |
AllocArray { dst, size, elem_size } | %p = alloca [N x T] (стек) / call @malloc (куча) |
Инструкции доступа к структурам/массивам:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
LoadField { dst, src, field } | %ptr = getelementptr T, T* %src, 0, field + load | |
StoreField { dst, field, src } | %ptr = getelementptr T, T* %dst, 0, field + store | |
LoadIndex { dst, src, index } | %ptr = getelementptr T, T* %src, 0, %index + load | |
StoreIndex { dst, index, src } | %ptr = getelementptr T, T* %dst, 0, %index + store | |
CreateStruct { dst, type_name, fields } | Цепочка insertvalue | Конструкция LLVM struct по порядку полей |
Инструкции преобразования типов:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Cast { dst, src, target_type } | bitcast / trunc / zext / sext / fptrunc / fpext / sitofp / fptosi / inttoptr / ptrtoint | Выбор подходящей инструкции cast по комбинации типов источника/назначения |
TypeTest(val, type) | — | Compile-time тест типа, генерирует icmp eq сравнения метки типа |
Инструкции владения и заимствования:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Borrow { dst, src, mutable } | — | Токен нулевого размера, полностью исчезает после компиляции, не создаёт никакого IR |
Release(val) | — | Токен нулевого размера, полностью исчезает после компиляции |
Move { dst, src } | — | Передача владения, после нормализации становится копированием регистров |
Drop(val) | call void @T.drop(T* %val) | Вызов деструктора типа (см. §7) |
ShareRef { dst, src } | call %T* @Arc_new(%src) / call %T* @Rc_new(%src) | Компилятор автоматически выбирает Arc/Rc в зависимости от того, кросспотоковый доступ или нет |
ArcNew { dst, src } | call %T* @Arc_new(%src) | Атомарный счётчик ссылок = 1 |
ArcClone { dst, src } | call %T* @Arc_clone(%src) | Атомарное инкрементирование счётчика ссылок |
ArcDrop(val) | call void @Arc_drop(%val) | Атомарное декрементирование + условное освобождение |
Инструкции параллелизма:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
Spawn { closures, plan, result } | Раскрывается в последовательность вызовов планировщика | Подробности в §5, runtime task_spawn + task_wait_all |
Yield | — | На AOT-пути spawn-блоки синхронно ожидают, не нуждаются в yield; игнорируется |
Инструкции unsafe-блоков и голых указателей:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
UnsafeBlockStart | — | Compile-time маркер, не генерирует IR |
UnsafeBlockEnd | — | Compile-time маркер, не генерирует IR |
PtrFromRef { dst, src } | %p = ptrtoint T* %src to i64 (или прямое копирование указателя) | |
PtrDeref { dst, src } | %v = load T, T* %src | |
PtrStore { dst, src } | store T %src, T* %dst | |
PtrLoad { dst, src } | %v = load T, T* %src |
Строковые инструкции:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
StringLength { dst, src } | %len = extractvalue { i8*, i64 } %src, 1 | String — это { ptr, len }, длина в поле 1 |
StringConcat { dst, lhs, rhs } | call String @yx_string_concat(%lhs, %rhs) | Вспомогательная функция runtime |
StringGetChar { dst, src, index } | getelementptr + load i32 | С проверкой границ |
StringFromInt { dst, src } | call String @yx_string_from_int(%src) | Вспомогательная функция runtime |
StringFromFloat { dst, src } | call String @yx_string_from_f64(%src) | Вспомогательная функция runtime |
Инструкции замыканий:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
MakeClosure { dst, func: String, env } | Выделение структуры замыкания + заполнение указателя функции (поиск по имени функции) и среды | { fn_ptr, env_fields... } |
LoadUpvalue { dst, upvalue_idx } | %v = extractvalue %env, upvalue_idx | Чтение upvalue из среды замыкания |
StoreUpvalue { src, upvalue_idx } | %env = insertvalue %env, %src, upvalue_idx | Запись в среу замыкания |
CloseUpvalue(val) | Копирование upvalue со стека в кучу |
Прочие инструкции:
| Инструкция IR | LLVM IR | Описание |
|---|---|---|
HeapAlloc { dst, type_id } | call i8* @malloc(i64 size) + запись метки типа | Выделение в куче + информация о типе |
NewDict { dst, keys, values } | call Dict @yx_dict_new(%keys, %values) | Вспомогательная функция runtime |
Примечание:
Push/Pop/Dup/Swapбыли устранены на этапе нормализации §4.0 и не появляются в таблице перевода.Borrow/Release— это токены compile-time нулевого размера, не создающие никакого машинного кода.
5. Генерация кода для блоков spawn
В соответствии с RFC-024, компиляция блока spawn делится на следующие шаги.
5.1 Обзор семантики
(r1, r2) = spawn {
t1 = fetch("url1"), // Прямое подвыражение → задача 1
t2 = fetch("url2"), // Прямое подвыражение → задача 2
return (t1, t2) // Синхронное ожидание, сборка результата
}Правила (RFC-024 §2.1):
- Прямые подвыражения блока spawn (операторы верхнего уровня, разделённые запятыми) создают параллельные задачи
- Выражения, вложенные в
{}, не являются прямыми подвыражениями и не становятся независимыми задачами - Весь блок spawn синхронно блокируется, ожидая завершения всех задач перед возвратом
5.2 Шаги компиляции
Step 1: Идентификация прямых подвыражений
Обход тела блока spawn, сбор операторов верхнего уровня
Step 2: Анализ зависимостей
Для каждого прямого подвыражения анализируется, какие переменные,
произведённые предыдущими задачами, оно использует
Нет зависимостей → можно немедленно планировать параллельно
Есть зависимости → постановка в очередь с ожиданием зависимой задачи
Step 3: Обнаружение конфликтов ресурсов (RFC-024 §2.5)
Проверка, используется ли один экземпляр типа ресурса несколькими задачами
Конфликт по одному экземпляру → пометка последовательного порядка выполнения
Step 4: Генерация функций задач
Каждое прямое подвыражение генерирует независимую LLVM-функцию (замыкание)
Step 5: Генерация кода планирования
Вызов runtime-функций планировщика task_spawn / task_wait
Step 6: Сборка результата
Сбор всех выходных данных задач, компоновка return-кортежа5.3 Модель генерации LLVM IR
; Вход в блок spawn
%task_count = 2
%tasks = alloca [2 x %TaskHandle]
; Создание задачи 1: fetch("url1")
%task1_fn = @spawn_closure_1
call @runtime_task_spawn(%tasks[0], %task1_fn, ...)
; Создание задачи 2: fetch("url2")
%task2_fn = @spawn_closure_2
call @runtime_task_spawn(%tasks[1], %task2_fn, ...)
; Синхронное ожидание всех задач
call @runtime_task_wait_all(%tasks, %task_count)
; Сборка возвращаемого значения
%r1 = call @runtime_task_result(%tasks[0])
%r2 = call @runtime_task_result(%tasks[1])
ret { %r1, %r2 }5.4 Зависимые задачи
result = spawn {
data = fetch("url"), // Задача 1: без зависимостей
processed = parse(data), // Задача 2: зависит от данных задачи 1
return processed
}Компилятор обнаруживает, что parse(data) использует data, произведённую задачей 1, и при генерации кода планирования помечает зависимость:
; Задача 2 создаётся с зависимостью от задачи 1
call @runtime_task_spawn_with_dep(%tasks[1], %task2_fn, %tasks[0])
; ↑
; Задача 0 (fetch) завершена5.5 Автоматическая сериализация по типу ресурса
Определённые в RFC-024 §2.5 типы ресурсов (FilePath, HttpUrl, DBUrl, Console и пользовательские типы ресурсов) автоматически сериализуются в блоках spawn:
(a, b) = spawn {
r1 = db.exec("SELECT ..."), // Использует SqliteDb (тип ресурса)
r2 = db.exec("INSERT ...") // Тот же экземпляр → автоматическая сериализация
}Компилятор обнаруживает, что один экземпляр ресурса используется двумя задачами, и генерирует последовательную зависимость:
; Задача 2 зависит от задачи 1 (автоматическая сериализация по ресурсу)
call @runtime_task_spawn_with_dep(%tasks[1], %task2_fn, %tasks[0])5.6 spawn for для данных параллелизма
results = spawn for item in items {
process(item)
}Компилятор раскрывает в N независимых задач (N = длина items), ограниченных максимальным параллелизмом.
6. Генерация FFI-кода
⚠️ Описание зависимости: Архитектура генерации FFI-кода, определённая в данном разделе (
native("x")→declare external @x→ marshalling-обёртка функция → call), стабильна и не меняется при изменениях синтаксиса RFC-026. Конкретные таблицы правил параметров marshalling (§6.2) и layouts непрозрачных типов (§6.3) ссылаются на определения из RFC-026 — если синтаксисnative()или правила marshalling в RFC-026 изменятся, достаточно обновить соответствующие таблицы映射 в данном документе, архитектурный уровень не затрагивается. Текущий статус RFC-026: на рассмотрении, находится в том же каталогеreview/.Условие принятия: До принятия данного RFC, части RFC-026, связанные с §6 данного документа (синтаксис объявлений
native(), правила параметров marshalling, layout непрозрачных типов{ i8* }, соглашение о привязках.drop), должны быть сначала заморожены или приняты вместе с 026. Иначе таблицы映射 в §6.2/§6.3/§7 могут устареть до начала реализации.
В соответствии с RFC-026, данный раздел определяет стратегию генерации LLVM IR для FFI-вызовов.
6.1 Объявление функций native()
sqlite3_open: (filename: String) -> SqliteDb = native("sqlite3_open")Компилируется в LLVM IR:
; Объявление внешней C-функции
declare i8* @sqlite3_open(i8*)
; YaoXiang-обёртывающая функция (обработка marshalling)
define { i8* } @__yx_sqlite3_open({ i8*, i64 } %filename) {
; marshalling: YaoXiang String → C string
%c_str = extractvalue { i8*, i64 } %filename, 0
; Вызов C-функции
%raw = call i8* @sqlite3_open(i8* %c_str)
; unmarshalling: C-указатель → непрозрачный тип
%result = insertvalue { i8* } undef, i8* %raw, 0
ret { i8* } %result
}Ключевые моменты:
native("sqlite3_open")→declare external @sqlite3_open- Компилятор автоматически генерирует marshalling-обёртывающую функцию
- Сигнатура обёртывающей функции использует типы YaoXiang, внутренне преобразуя в типы C
6.2 Marshalling параметров
| Направление | Преобразование |
|---|---|
YaoXiang String → C char* | Извлечение поля .ptr для передачи |
YaoXiang Int32 → C int | Прямая передача (i32) |
YaoXiang *Void → C void* | Прямая передача (i8*) |
YaoXiang T (прозрачный тип) → C struct T* | Передача адреса |
YaoXiang T (непрозрачный тип) → C struct T* | Извлечение указателя из { i8* } для передачи |
6.3 Layout LLVM для непрозрачных типов
Непрозрачные типы, определённые в RFC-026 §4.1:
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void
}
return SqliteDb
}LLVM layout: { i8* } — структура, содержащая C-указатель.
Оптимизация layout: Когда непрозрачный тип имеет только одно поле handle: *Void, его можно оптимизировать до прямого использования i8* (опуская внешнюю struct). Оптимизированный ABI полностью совместим с C-указателем, нулевые накладные расходы marshalling. Компилятор включает эту оптимизацию по умолчанию, пользователь этого не замечает.
6.4 LLVM-представление возвращаемого значения ?T (nullable)
FFI nullable возвращаемое значение, определённое в RFC-026 §7.6:
sqlite3_open: (filename: String) -> ?SqliteDb = native("sqlite3_open")Универсальное LLVM-представление: { i1, { i8* } } — флаг наличия + данные.
Оптимизация для FFI null-указателей: Если ?T имеет T как непрозрачный тип (внутренне указатель), компилятор использует оптимизацию null-указатель = None:
; Оптимизированное LLVM-представление: прямое использование nullable-указателя
define i8* @__yx_sqlite3_open(...) {
%raw = call i8* @sqlite3_open(...)
; null → None, не-null → Some (обёрнут в непрозрачный тип)
ret i8* %raw
}Вызывающая сторона:
%raw = call i8* @__yx_sqlite3_open(...)
%is_null = icmp eq i8* %raw, null
br i1 %is_null, label %none_branch, label %some_branchДанная оптимизация делает FFI-вызовы ?SqliteDb с нулевыми дополнительными накладными расходами — полностью эквивалентно проверке null в C.
6.5 Интеграция yx-bindgen
Автогенерируемые файлы привязок yx-bindgen §6 обрабатываются компилятором как обычный исходный код YaoXiang во время компиляции. Компилятору не нужно знать, что код получен от bindgen — обработка объявлений native() и определений типов unsafe {} полностью идентична.
7. Генерация кода деструкторов
В соответствии с RAII-семантикой RFC-009 и соглашением о .drop из RFC-026 §7.
7.1 Распознавание привязки .drop
SqliteDb.drop = sqlite3_close[0]Компилятор распознаёт привязку .drop и помечает указатель на деструктор в метаданных типа.
7.2 Вставка Cleanup при окончании области видимости
Пользовательский код:
{
db = SqliteDb.open("test.db")
stmt = db.prepare("SELECT ...")
stmt.step()
// ← Конец области видимости
}
Вставка cleanup компилятором (в обратном порядке):
call @sqlite3_finalize(%stmt) // stmt.drop()
call @sqlite3_close(%db) // db.drop()Места вставки:
- Нормальное окончание области видимости (
}) - Досрочный возврат (перед
return) - Путь распространения ошибки
?(перед?) - Окончание блока spawn (деструктуризация переменных внутри задачи)
7.3 Move и деструктуризация
db = SqliteDb.open("test.db")
db2 = db // Move: владение передаётся db2
// db уже недействителен, drop для db не вставляется
// ← Конец области видимости: drop вставляется только для db2Компилятор отслеживает семантику Move (RFC-009 §1) и вставляет вызов деструктора только в месте финального владельца переменной.
7.4 Обработка ошибок деструктора
; Режим debug: проверка возвращаемого значения деструктора
%ret = call i32 @sqlite3_close(i8* %handle)
%ok = icmp eq i32 %ret, 0
br i1 %ok, label %done, label %panic
panic:
call @__yx_panic("destructor failed")
unreachable
done:
ret void
; Режим release: игнорирование возвращаемого значения
call i32 @sqlite3_close(i8* %handle)
ret void8. Структура артефактов компиляции
Артефакты компиляции содержат следующие компоненты (конкретные определения struct определяются на этапе реализации):
- Машинный код: Объектные файлы LLVM (.o), содержащие результаты перевода всех функций
- spawn-метаданные: Указатели на функции задач для каждого блока spawn, зависимости, пары сериализации конфликтов ресурсов
- FFI-таблица символов: Внешние ссылки на C-символы (имя символа + является ли слабой ссылкой)
- Таблица точек входа: Список функций входа исполняемого файла
- Информация о типах: Рефлексивные метаданные, записываемые в сегмент
.reflect, загружаемые по запросу через mmap
9. Runtime-библиотека
В соответствии с RFC-008 §6.2, runtime линкуется в итоговый exe в виде статической библиотеки.
Внутренняя структура итогового exe:
┌────────────────────────────────────────────┐
│ Пользовательский код (нативный машинный код) │
│ ├── Обычные функции (последовательное выполнение) │
│ ├── Раскрытие блоков spawn (функции задач + вызовы планировщика) │
│ ├── FFI marshalling-обёртывающие функции │
│ └── RAII код деструктуризации │
├────────────────────────────────────────────┤
│ Статическая библиотека runtime (≈ 500KB-1MB, зависит от платформы и выбранных возможностей) │
│ ├── Thread pool (num_workers) │
│ ├── Event loop (libuv / io_uring) │
│ ├── Work-stealing queue (только Full Runtime) │
│ ├── Memory allocator (jemalloc / mimalloc) │
│ └── Рефлексивные метаданные (сегмент .reflect, загрузка по запросу mmap) │
│ │
│ Нет: │
│ ❌ Байткодовый интерпретатор │
│ ❌ JIT-компилятор │
│ ❌ Сборщик мусора │
│ ❌ Виртуальная машина │
└────────────────────────────────────────────┘Ключевой дизайн: Распознавание задач блока spawn и анализ зависимостей выполняются на этапе компиляции, runtime выполняет только "создать задачу → распределить в thread pool → ожидать завершения" — структуры данных фиксированы, поведение предсказуемо.
Разница с оценкой размера RFC-008: RFC-008 §4 оценивал планировщик в 200-500KB, включающий только ядро планирования задач. Оценка 500KB-1MB в данном документе дополнительно включает memory allocator (jemalloc/mimalloc), event loop (libuv/io_uring) и сегмент рефлексивных метаданных. Фактический размер зависит от платформы и выбранных возможностей, на этапе реализации будут даны точные цифры.
Три уровня runtime и связь с LLVM (в соответствии с RFC-008 §1):
| Runtime | Поведение LLVM AOT |
|---|---|
| Embedded | Без поддержки spawn, прямое генерирование последовательного машинного кода |
| Standard | Поддержка блоков spawn, планирование в рамках одного потока (num_workers=1) |
| Full | Поддержка блоков spawn, многопоточное планирование (num_workers>1), поддержка WorkStealing |
Детальный дизайн
Структура каталогов модулей
В соответствии с layout каталогов из RFC-008 §6. Маркер [! plan] указывает, что файл/каталог ещё не создан и будет добавлен на этапе реализации данного RFC.
src/
├── frontend/ # Компиляция переднего плана (общая для всех бэкендов)
│ ├── core/
│ │ ├── spawn/ # Модуль spawn (анализ параллелизма, общий для VM и LLVM)
│ │ │ ├── mod.rs # Вход модуля spawn
│ │ │ ├── placement.rs # Проверка легитимности размещения spawn
│ │ │ └── analysis.rs # [! plan] Распознавание задач, анализ зависимостей, обнаружение конфликтов ресурсов
│ │ └── typecheck/
│ │ └── ...
│
├── middle/
│ ├── core/
│ │ ├── ir.rs # Определение IR (общий для VM и LLVM)
│ │ └── ir_gen.rs # Генерация IR
│ └── passes/
│ ├── codegen/
│ │ ├── mod.rs # Orchestration layer (текущий вывод BytecodeFile)
│ │ ├── translator.rs # Перевод IR → байткод (для VM-бэкенда)
│ │ ├── emitter.rs # Emit байткода + backpatching переходов (для VM-бэкенда)
│ │ ├── buffer.rs # Constant pool + буфер байткода (для VM-бэкенда)
│ │ ├── bytecode.rs # Определение формата байткода + сериализация (для VM-бэкенда)
│ │ ├── flow.rs # Распределение регистров + генерация меток + таблица символов (для VM-бэкенда)
│ │ └── operand.rs # Парсинг операндов (для VM-бэкенда)
│ ├── lifetime/ # Анализ времени жизни/активности токенов
│ └── mono/ # Мономорфизация
│
├── backends/
│ ├── common/ # Общие значения/куча/опкоды
│ ├── interpreter/ # Tree-walking интерпретатор (VM-бэкенд)
│ ├── llvm/ # [! plan] LLVM-бэкенд кодогенерации (см. список файлов ниже)
│ │ ├── mod.rs # [! plan] Вход LLVM-бэкенда
│ │ ├── context.rs # [! plan] Управление LLVM-контекстом
│ │ ├── types.rs # [! plan] Отображение типов (YaoXiang → LLVM IR)
│ │ ├── values.rs # [! plan] Отображение значений
│ │ ├── func.rs # [! plan] Перевод функций
│ │ ├── spawn.rs # [! plan] Раскрытие блоков spawn
│ │ ├── ffi.rs # [! plan] Генерация FFI-кода вызовов
│ │ └── drop.rs # [! plan] Вставка деструкторов
│ └── runtime/ # Compile-time runtime (статическая библиотека, линкуется в exe)
│ ├── engine.rs # Движок планирования задач
│ ├── facade.rs # Внешний интерфейс
│ └── task.rs # Представление задачи
│
└── util/
└── diagnostic/ # Диагностика ошибок (общая)Ключевое изменение: Анализ блоков spawn (распознавание задач, анализ зависимостей, обнаружение конфликтов ресурсов) будет реализован в
frontend/core/spawn/(передний план). Существующийfrontend/core/typecheck/passes/spawn_placement.rs(проверка легитимности размещения spawn) будет перенесён вfrontend/core/spawn/placement.rs, подробности в RFC-024. LLVM-бэкенд только потребляет результаты анализа и генерирует соответствующий код планирования.Пояснение текущего состояния: Текущие файлы в
middle/passes/codegen/—buffer.rs,emitter.rs,bytecode.rs,flow.rs,operand.rs— обслуживают байткодовую генерацию VM-бэкенда (CodegenContext::generate()→BytecodeFile). LLVM-бэкенд будет реализован вbackends/llvm/, на одном уровне с interpreter-бэкендом и runtime — оба используют один и тот же входModuleIR, выводя разные целевые форматы (байткод vs нативный код).
Поддержка платформенного ABI
| Платформа | Target triple | Формат вывода | Соглашение о вызовах (FFI по умолчанию) |
|---|---|---|---|
| Linux x86_64 | x86_64-unknown-linux-gnu | ELF | System V AMD64 |
| macOS x86_64 | x86_64-apple-darwin | Mach-O | System V AMD64 |
| macOS ARM64 | aarch64-apple-darwin | Mach-O | ARM64 AAPCS |
| Windows x86_64 | x86_64-pc-windows-msvc | COFF | Microsoft x64 |
FFI-вызовы по умолчанию используют платформенное C-соглашение о вызовах. Пользователь может переопределить через опции типа native("symbol", cc = "stdcall") (в соответствии с будущими расширениями RFC-026).
Согласованность семантики чисел с плавающей запятой (VM ↔ LLVM)
Ключевое обещание архитектуры с двумя бэкендами — согласованность поведения VM (разработка и отладка) и LLVM (production). Операции с плавающей запятой имеют потенциальные точки несоответствия в двух режимах выполнения:
| Сценарий | Риск | Стратегия |
|---|---|---|
| Распространение NaN | VM и LLVM могут по-разному обрабатывать знаковый бит и payload NaN | Компилятор нормализует представление NaN на уровне IR, сравнения NaN统一使用 fcmp uno |
| Режим округления | LLVM по умолчанию round-to-nearest-even, VM зависит от host CPU | Не экспонировать нестандартные режимы округления, VM и LLVM统一 используют RTNE |
| Деление на ноль | IEEE 754 определяет ±Inf, но некоторые платформы могут trap | Режим debug проверяет деление на ноль и выдаёт диагностику; режим release следует IEEE 754 |
-0.0 vs +0.0 | Операции сравнения могут быть неэквивалентными | 统一使用 правило IEEE 754: +0.0 == -0.0 |
| Денормализованные числа | Некоторые платформы flush-to-zero | LLVM не включает атрибут denormal-fp-math, сохраняет полную семантику IEEE 754 |
Стратегия тестирования: Реализовать набор тестов согласованности floating-point для всех бэкендов — один и тот же исходный код YaoXiang выполняется соответственно на VM и LLVM бэкендах, выходные данные сравниваются поэлементно. Эти тесты являются обязательным gate в CI.
Компромиссы
Преимущества
- Производительность: AOT-компиляция быстрее интерпретируемого выполнения в 10-100x
- Унифицированный передний план: VM и LLVM совместно используют один и тот же передний план, поведение полностью согласовано
- Нулевые накладные расходы планирования: Обычный код напрямую генерирует последовательный машинный код, вне блоков spawn нет накладных расходов DAG
- Статическая линковка: Нет зависимостей от внешнего runtime, один exe для развёртывания
- Нулевой GC: RAII детерминированная деструктуризация, без пауз
- FFI нулевые накладные расходы: Оптимизация null-указателей
?T, оптимизация layout непрозрачных типов, стоимость FFI-вызовов эквивалентна C - Compile-time анализ: Распознавание задач блока spawn и анализ зависимостей выполняются на этапе компиляции, runtime только выполняет
Недостатки
- Сложность интеграции LLVM: Требуется глубокое понимание inkwell API и LLVM IR
- Время компиляции: AOT-компиляция медленнее интерпретатора (разовый компромисс)
- Опыт отладки: Отладка нативного кода требует поддержки символов DWARF/PDB (компилятор должен генерировать отладочную информацию)
- Инкрементная компиляция: Инкрементная компиляция крупных проектов требует дополнительного проектирования
- Согласованность семантики floating-point: VM и LLVM могут иметь различия в пограничном поведении, таком как распространение NaN, режимы округления, деление на ноль и т.д., необходимо обеспечить согласованность обоих бэкендов через стратегии нормализации (см. §10)
Согласованность со связанными RFC
| RFC | Согласованность |
|---|---|
| RFC-024 spawn-блок параллелизм | ✅ Прямые подвыражения блока spawn → распределение задач |
| RFC-008 архитектура runtime | ✅ Двойной бэкенд + статическая библиотека планировщика + структура каталогов модулей |
| RFC-009 модель владения v9 | ✅ Токены &T/&mut T (нулевой размер), ref T (fat-указатель), ?T (Option) |
| RFC-026 базовый механизм FFI | ✅ native() → declare + marshalling, .drop → RAII cleanup |
Альтернативные решения
| Решение | Описание | Почему не выбрано |
|---|---|---|
| Только интерпретатор | AOT не требуется | Недостаточно высокая производительность |
| Чистая статическая компиляция (без runtime) | Без линковки планировщика | Блоки spawn требуют runtime-планирования задач |
| Cranelift-бэкенд | Более быстрая компиляция | Производительность runtime ниже, чем у LLVM, как будущий опциональный бэкенд |
| Линковка внешнего LLVM runtime | Использование встроенного LLVM runtime | Вводит ненужные зависимости |
Стратегия реализации
Фазы разделения
Фаза 1: Базовая структура
- [ ] Добавить зависимость inkwell
- [ ] Реализовать инициализацию LLVM-контекста (
context.rs) - [ ] Реализовать базовое отображение типов (
types.rs)
Фаза 2: Перевод функций
- [ ] Реализовать перевод объявлений функций (
func.rs) - [ ] Реализовать перевод базовых инструкций (арифметика, управление потоком, вызовы) (
translator.rs) - [ ] Реализовать отображение значений (
values.rs)
Фаза 3: Перевод типов владения
- [ ] Реализовать токены
&T/&mut T(нулевой размер, исчезают после компиляции) - [ ] Реализовать
ref T(fat-указатель{ i64*, T* }) - [ ] Реализовать
?T(tagged union{ i1, T }) - [ ] Реализовать
List(T)({ T*, i64, i64 }) - [ ] Реализовать отслеживание семантики Move (для определения вставки деструкторов)
Фаза 4: Генерация кода блоков spawn
- [ ] Потреблять результаты анализа из
spawn_placement.rs - [ ] Прямые подвыражения → генерация функций задач
- [ ] Генерация кода планирования зависимых задач
- [ ] Сериализация конфликтов ресурсов
- [ ] Раскрытие spawn for
Фаза 5: Генерация FFI-кода
- [ ]
native()→declare external(ffi.rs) - [ ] marshalling параметров / unmarshalling возвращаемых значений
- [ ] Layout непрозрачных типов (включая оптимизацию однослойных)
- [ ] Оптимизация null-указателей
?T(специально для FFI)
Фаза 6: Генерация кода деструкторов
- [ ] Распознавание привязки
.drop - [ ] Вставка cleanup при окончании области видимости (в обратном порядке) (
drop.rs) - [ ] Cleanup на путях досрочного возврата
- [ ] Cleanup на путях распространения ошибки
?
Фаза 7: Линковка runtime-библиотеки
- [ ] Реализовать runtime-функции
runtime_task_spawn/runtime_task_wait_allи т.д. - [ ] Линковка статической библиотеки runtime
- [ ] Интеграционное end-to-end тестирование
Зависимости
- RFC-024 (spawn-блок параллелизм) → вход фазы 4
- RFC-009 v9 (владение) → вход фаз 3, 6
- RFC-008 (архитектура runtime) → вход фазы 7
- RFC-026 (механизм FFI) → вход фазы 5
Связанные работы
Lazy Task Creation (1990)[^1]
| Атрибут | Описание |
|---|---|
| Организация | MIT |
| Автор | James R. Larus, Robert H. Halstead Jr. |
| Ядро | Отложенное создание подзадач по требованию |
| Ценность | Теоретическая основа планирования задач по требованию в блоках spawn |
Основная идея: Вместо немедленного создания задачи — отложенное создание. Когда родительская задача нуждается в значении дочерней задачи, тогда и создаётся дочерняя задача. Это решает проблему накладных расходов производительности задач с мелкой гранулярностью[^1]. Планировщик блоков spawn YaoXiang заимствует эту идею — задачи распознаются на этапе компиляции, но распределяются в thread pool по требованию во время выполнения.
Lazy Scheduling (2014)[^2]
| Атрибут | Описание |
|---|---|
| Организация | University of Maryland |
| Автор | Tzannes, Caragea |
| Ядро | Runtime адаптивное планирование, без дополнительного состояния |
| Ценность | Ссылка на проектирование планировщика WorkStealing Full Runtime |
Язык SISAL[^3]
| Атрибут | Описание |
|---|---|
| Организация | Lawrence Livermore National Laboratory (LLNL) |
| Ядро | Язык single-assignment, Dataflow graph, implicit parallelism |
| Ценность | Доказательство осуществимости модели Dataflow в индустриальных приложениях |
Ключевое различие: Параллелизм в SISAL неявный — язык имеет семантику single-assignment, компилятор автоматически анализирует граф data-dependency всей программы для определения параллелизма. Параллелизм в YaoXiang явный — пользователь помечает параллельные области блоком spawn {}, компилятор анализирует зависимости только внутри блока spawn. Это позволяет избежать сложности анализа всей программы SISAL, сохраняя при этом контроль пользователя над параллельным поведением.
Схема параллелизма Mul-T[^4]
| Атрибут | Описание |
|---|---|
| Организация | MIT |
| Ядро | Конструкция Future, реализация Lazy Task Creation |
| Ценность | Конкретная ссылка на реализацию |
Сравнительное резюме
| Технология | Отложенное создание | Маркер параллелизма | Область анализа | Владение |
|---|---|---|---|---|
| Lazy Task Creation[^1] | ✅ | Неявный | Вся программа | N/A |
| Lazy Scheduling[^2] | ✅ | Неявный | Вся программа | N/A |
| SISAL[^3] | ✅ | Неявный (single-assignment) | Вся программа | N/A |
| Mul-T[^4] | ✅ | Явный (future) | Точка вызова | N/A |
| YaoXiang | ✅ | Явный (блок spawn) | Внутри блока spawn | ✅ (Move + токены + ref) |
Инновация YaoXiang: Поднятие маркера параллелизма с "каждый вызов функции" (future) до "структурированный блок" (spawn), пользователь пишет обычный код, блок spawn помещается там, где нужен параллелизм. Область анализа ограничена блоком spawn, компиляция эффективна, а поведение контролируемо.
Приложения
Приложение A: Сравнение с async Rust
| Характеристика | Rust async | YaoXiang LLVM AOT |
|---|---|---|
| Артефакты компиляции | State machine + машинный код | Машинный код + spawn-метаданные задач |
| Runtime | tokio | Статически линкованный планировщик (≈ 500KB-1MB) |
| Маркер параллелизма | async/await ключевые слова | Блоки spawn { } |
| Создание задач | State machine генерируется compile-time | Распознавание прямых подвыражений compile-time → функции задач |
| "Цветные" функции | async заразность | Нет окрашивания функций |
| Синхронное ожидание | .await | Автоматическая синхронная блокировка блока spawn |
| Управление памятью | GC (runtime) | RAII (детерминированное) |
| Механизм совместного использования | Arc::new() + ручной Weak | ref ключевое слово (компилятор автоматически выбирает Rc/Arc) |
Приложение B: Журнал решений по дизайну
| Решение | Решение | Дата |
|---|---|---|
| Принятие LLVM AOT | Прямая кодогенерация, без чрезмерных абстракций | 2026-02-15 |
| Согласование модели параллелизма | Согласование с моделью прямых подвыражений блока spawn из RFC-024 | 2026-06-10 |
| Область анализа DAG | Внутри блока spawn, не пересекая блоки spawn (согласование с RFC-024) | 2026-06-05 |
| Согласование модели владения | Согласование с RFC-009 v9: токены &T/&mut T + ключевое слово ref | 2026-06-10 |
| Модель двойного бэкенда | VM (разработка) + LLVM (production), согласование с RFC-008 | 2026-05-11 |
| Форма планировщика | Статическая библиотека, линкуется в exe, ≈ 500KB-1MB (зависит от платформы и возможностей), без GC | 2026-05-11 |
| Генерация FFI-кода | Интеграция RFC-026: объявления native() + marshalling | 2026-06-10 |
| Деструкторы | .drop → вставка RAII cleanup, согласование с RFC-026 §7 | 2026-06-10 |
| Обработка побочных эффектов | Удаление вывода @IO/@Pure, переход на типы ресурсов из RFC-024 | 2026-06-10 |
| Рефлексивные метаданные | Компилируются в сегмент .reflect exe, загрузка по требованию через mmap | 2026-05-11 |
| Цитирование статей | Сохранение Lazy Task Creation и др., чёткое указание отличий YaoXiang | 2026-02-16 |
Ссылки
[^1]: Larus, J. R., & Halstead, R. H. (1990). Lazy Task Creation: A Technique for Increasing the Granularity of Parallel Programs. MIT.
[^2]: Tzannes, A., & Caragea, G. (2014). Lazy Scheduling: A Runtime Adaptive Scheduler for Declarative Parallelism. University of Maryland.
[^3]: Feo, J. T., et al. (1990). A report on the SISAL language project. Lawrence Livermore National Laboratory.
[^4]: Mohr, E., et al. (1991). Mul-T: A high-performance parallel lisp. MIT.
- inkwell LLVM bindings
- RFC-024: Модель параллелизма на основе блоков spawn
- RFC-008: Модель параллелизма Runtime и decoupled планировщик
- RFC-009: Проектирование модели владения
- RFC-026: Базовый механизм FFI
Жизненный цикл и судьба
| Статус | Расположение | Описание |
|---|---|---|
| Черновик | docs/design/rfc/ | Авторский черновик, ожидает подачи на рецензирование |
| На рецензии | docs/design/rfc/review/ | Открытое обсуждение сообщества и обратная связь |
| Принято | docs/design/rfc/accepted/ | Становится официальным документом дизайна |
| Отклонено | docs/design/rfc/ | Сохраняется в каталоге RFC |
Текущий статус: Принято — согласовано с моделью параллелизма блоков spawn из RFC-024, моделью владения v9 из RFC-009, механизмом FFI из RFC-026
