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 — для промышленного выпуска.
Основные обязанности:
Исходный код → Фронтенд (общий) → IR → LLVM Codegen → .o → Линковка статической библиотеки планировщика → exeКомпилятор преобразует исходный код YaoXiang в нативный машинный код, где:
| Языковая возможность | Стратегия компиляции |
|---|---|
| Обычный код | Последовательный машинный код, нулевые накладные расходы на планирование |
Блок spawn { } | Прямое подвыражение → распределение задач + синхронное ожидание (согласно RFC-024) |
native("symbol") | LLVM declare external + marshalling аргументов (согласно RFC-026) |
Деструктор .drop | Вставка RAII-кода очистки (согласно RFC-009) |
Токены &T / &mut T | Типы нулевого размера, исчезают после компиляции |
Разделяемое ref T | Толстый указатель { 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-100 раз медленнее машинного кода |
| Сложность развёртывания | Необходимо поставлять интерпретатор и среду исполнения |
| Производственная среда | Интерпретатор не подходит для сценариев, чувствительных к производительности |
LLVM в модели с двумя бэкендами
RFC-008 §6 определяет архитектуру с двумя бэкендами:
┌─────────────────────┐
│ Фронтенд (общий) │
│ Lexer → Parser │
│ → TypeCheck │
│ → анализ spawn │
│ → анализ побегов │
└──────────┬──────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Бэкенд VM │ │ Бэкенд LLVM │
│ (разработка) │ │ (производство) │
│ 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-оптимизации + генерация целевого кода
→ Линковка статической библиотеки среды исполнения → исполняемый файл2. Процесс компиляции
Фаза 1: Фронтенд (общий с бэкендом VM)
- Разбор, проверка типов, анализ spawn-блоков, анализ побегов
- Выход: IR с аннотациями типов
Фаза 2: Генерация LLVM IR
- Отображение типов, объявления функций, трансляция инструкций
- Выход: LLVM Module
Фаза 3: Оптимизации LLVM
- Стандартный конвейер оптимизаций LLVM (O0/O1/O2/O3)
- Инлайнинг, свёртка констант, удаление мёртвого кода
Фаза 4: Генерация целевого кода
- LLVM TargetMachine → файлы .o
- Платформы: Linux (ELF), macOS (Mach-O), Windows (COFF)
Фаза 5: Линковка
- Линковка статической библиотеки среды исполнения (планировщик, аллокатор)
- Выход: исполняемый файл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-кодовая позиция |
String | { i8*, i64 } | Указатель + длина в байтах |
Void | {} | Пустой тип нулевого размера |
&T | — | Токен нулевого размера, исчезает после компиляции, не порождает IR |
&mut T | — | Токен нулевого размера, исчезает после компиляции, не порождает IR |
ref T | { i64*, T* } | Толстый указатель (указатель счётчика ссылок + указатель данных) |
*T | T* | Голый указатель |
[T; N] | [N x T] | Массив фиксированной длины |
List(T) | { T*, i64, i64 } | Указатель данных + длина + ёмкость |
| Структура | Соответствующий LLVM struct | Поля располагаются в порядке объявления |
| Перечисление-структура | { i64, [max_payload_size] } | Тег + union максимального payload |
?T | { i1, T } | Флаг наличия значения + данные (универсальное представление) |
| Непрозрачный тип FFI | { i8* } | Обёртка над C-указателем |
| Указатель на функцию | T (...)* | Тип указателя на функцию |
Нулевые накладные расходы
&T/&mut T: RFC-009 §2.7 определяет, что компилятор внутренне назначает токенам идентификаторы брендов (уникальные целые на этапе компиляции); после мономорфизации и инлайнинга бренды полностью исчезают — в сгенерированном машинном коде не остаётся никаких следов токенов.
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 Таблица трансляции инструкций
Ниже приведена стратегия трансляции каждого варианта перечисления Instruction в LLVM IR. Имена инструкций полностью совпадают с src/middle/core/ir.rs.
Арифметические инструкции:
| IR-инструкция | LLVM IR | Описание |
|---|---|---|
Add { dst, lhs, rhs } | add (целое) / fadd (с пл. точкой) | По типу выбирается целочисленное или вещественное сложение |
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 (вещ.) |
Анонс правки строки
Mod(заметка от 2026-09-22, согласно RFC-011b):srem/urem— это остаток от усечения (знак следует за делимым), и в столбце «Описание» термин «остаток от деления» используется неточно. RFC-011b подтвердил, что установленная семантика%— это математический остаток от деления (знак результата следует за делителем, в таблице приоритетов справочника языка «умножение/деление/остаток» имеет приоритет). После закрепления этой семантики отображение в данной строке должно быть изменено наsrem+ последовательность коррекции знака (илиsdiv+mul+sub), а сторона реализации (интерпретаторchecked_rem, свёртка констант, байткодI64_REM) должна быть изменена синхронно.
Побитовые инструкции:
| 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 } | vtable GEP + 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) | — | Проверка типа на этапе компиляции, порождает 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, во время исполнения task_spawn + task_wait_all |
Yield | — | На пути AOT spawn-блок ожидает синхронно, yield не нужен; игнорируется |
Unsafe-блоки и инструкции голых указателей:
| IR-инструкция | LLVM IR | Описание |
|---|---|---|
UnsafeBlockStart | — | Метка этапа компиляции, IR не порождает |
UnsafeBlockEnd | — | Метка этапа компиляции, 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) | Вспомогательная функция среды исполнения |
StringGetChar { dst, src, index } | getelementptr + load i32 | С проверкой границ |
StringFromInt { dst, src } | call String @yx_string_from_int(%src) | Вспомогательная функция среды исполнения |
StringFromFloat { dst, src } | call String @yx_string_from_f64(%src) | Вспомогательная функция среды исполнения |
Инструкции замыканий:
| 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 | Запись upvalue в окружение замыкания |
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) | Вспомогательная функция среды исполнения |
Примечание:
Push/Pop/Dup/Swapуже устранены на этапе нормализации §4.0 и не появляются в таблице трансляции.Borrow/Release— это токены нулевого размера на этапе компиляции, не порождающие никакого машинного кода.
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 Шаги компиляции
Шаг 1: Идентификация прямых подвыражений
Обход тела spawn-блока, сбор операторов верхнего уровня
Шаг 2: Анализ зависимостей
Для каждого прямого подвыражения анализ, на какие переменные,
порождённые предыдущими задачами, оно ссылается
Без зависимостей → может быть диспетчеризовано немедленно параллельно
С зависимостями → ставится в очередь после задач-зависимостей
Шаг 3: Обнаружение конфликтов ресурсов (RFC-024 §2.5)
Проверка, не используется ли один и тот же экземпляр ресурса
несколькими задачами
Конфликт по одному экземпляру → отметка о последовательном
порядке выполнения
Шаг 4: Генерация функций задач
Каждое прямое подвыражение порождает отдельную LLVM-функцию (замыкание)
Шаг 5: Генерация кода диспетчеризации
Вызовы task_spawn / task_wait планировщика среды исполнения
Шаг 6: Сборка результатов
Сбор выходов всех задач, формирование возвращаемого кортежа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: зависит от data от задачи 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) и компоновки непрозрачных типов (§6.3) ссылаются на определения RFC-026 — если синтаксисnative()или правила marshalling в RFC-026 изменятся, потребуется обновить только соответствующую таблицу отображения в данном документе; архитектурный уровень не пострадает. Текущий статус RFC-026: на рассмотрении, находится в каталогеreview/вместе с данным документом.Условие принятия: до принятия данного RFC части RFC-026, относящиеся к §6 данного документа (синтаксис объявления
native(), правила marshalling аргументов, компоновка непрозрачного типа{ 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 LLVM-компоновка непрозрачных типов
Непрозрачный тип, определённый в RFC-026 §4.1:
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void
}
return SqliteDb
}LLVM-компоновка: { i8* } — структура, содержащая C-указатель.
Оптимизация компоновки: если непрозрачный тип имеет единственное поле handle: *Void, его можно оптимизировать до прямого использования i8* (без обрамляющей структуры). Оптимизированный ABI полностью совпадает с C-указателем, накладные расходы на marshalling нулевые. Эта оптимизация включена по умолчанию и прозрачна для пользователя.
6.4 LLVM-представление nullable-возвращаемого значения ?T
FFI nullable-возвращаемое значение, определённое в RFC-026 §7.6:
sqlite3_open: (filename: String) -> ?SqliteDb = native("sqlite3_open")Универсальное LLVM-представление: { i1, { i8* } } — флаг наличия + данные.
Оптимизация для FFI с null-указателем: если T в ?T — непрозрачный тип (внутри указатель), компилятор применяет оптимизацию null-указатель = None:
; Оптимизированное LLVM-представление: прямое использование допускающего null указателя
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 Вставка очистки в конце области видимости
Пользовательский код:
{
db = SqliteDb.open("test.db")
stmt = db.prepare("SELECT ...")
stmt.step()
// ← конец области видимости
}
Вставленная компилятором очистка (в обратном порядке):
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 недействителен, для db деструктор не вставляется
// ← конец области видимости: деструктор вставляется только для db2Компилятор отслеживает семантику Move (RFC-009 §1) и вставляет вызов деструктора только в месте конечного владельца переменной.
7.4 Обработка ошибок деструктора
; режим отладки: проверка возвращаемого значения деструктора
%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. Структура артефактов компиляции
Артефакты компиляции включают следующие составляющие (конкретные определения структур будут уточнены на этапе реализации):
- Машинный код: объектные файлы (
.o), скомпилированные LLVM, содержащие результаты трансляции всех функций - Метаданные spawn: указатели функций задач, отношения зависимостей, пары сериализации по конфликтам ресурсов для каждого spawn-блока
- Таблица символов FFI: ссылки на внешние C-символы (имя символа + флаг слабой ссылки)
- Таблица точек входа: список функций входа исполняемого файла
- Информация о типах: метаданные рефлексии, записываются в секцию
.reflect, mmap по запросу во время исполнения
9. Библиотека среды исполнения
Согласно RFC-008 §6.2, среда исполнения линкуется как статическая библиотека в финальный exe.
Внутренняя структура финального exe:
┌────────────────────────────────────────────┐
│ Пользовательский код (нативный машинный код) │
│ ├── Обычные функции (последовательное выполнение) │
│ ├── Развёртка spawn-блоков │
│ │ (функции задач + вызовы планировщика) │
│ ├── Функции-обёртки FFI marshalling │
│ └── RAII-код деструкторов │
├────────────────────────────────────────────┤
│ Статическая библиотека среды исполнения │
│ (около 500KB-1MB, зависит от платформы │
│ и набора функций) │
│ ├── Пул потоков (num_workers) │
│ ├── Цикл событий (libuv / io_uring) │
│ ├── Очереди Work-Stealing │
│ │ (только Full Runtime) │
│ ├── Аллокатор памяти (jemalloc / mimalloc)│
│ └── Метаданные рефлексии (секция .reflect, │
│ mmap по запросу) │
│ │
│ Отсутствует: │
│ ❌ Байткод-интерпретатор │
│ ❌ JIT-компилятор │
│ ❌ GC │
│ ❌ Виртуальная машина │
└────────────────────────────────────────────┘Ключевой замысел: идентификация задач spawn-блока и анализ зависимостей завершаются на этапе компиляции, во время исполнения остаётся лишь «создать задачу → распределить в пул потоков → дождаться завершения» — структуры данных фиксированы, поведение предсказуемо.
Отличие от оценки размера в RFC-008: в RFC-008 §4 планировщик оценивается в 200-500KB и включает только ядро диспетчеризации задач. Оценка 500KB-1MB в данном документе дополнительно включает аллокатор памяти (jemalloc/mimalloc), цикл событий (libuv/io_uring) и секцию метаданных рефлексии. Фактический размер зависит от платформы и набора функций; точные цифры будут даны на этапе реализации.
Связь трёх уровней среды исполнения с LLVM (согласно RFC-008 §1):
| Среда исполнения | Поведение LLVM AOT |
|---|---|
| Embedded | Без поддержки spawn, напрямую порождается последовательный машинный код |
| Standard | Поддержка spawn-блоков, DAG внутри spawn + однопоточное планирование (num_workers=1) |
| Full | Поддержка spawn-блоков, DAG внутри spawn + многопоточное планирование (num_workers>1), поддержка WorkStealing |
Детальное проектирование
Структура каталогов модулей
Согласно компоновке каталогов RFC-008 §6. Метка [! планируется] означает, что данный файл/каталог ещё не создан и будет введён на этапе реализации данного RFC.
src/
├── frontend/ # Фронтенд компилятора (общий для всех бэкендов)
│ ├── core/
│ │ ├── spawn/ # модуль spawn (общий анализ параллелизма для VM и LLVM)
│ │ │ ├── mod.rs # вход в модуль spawn
│ │ │ ├── placement.rs # проверка допустимости позиций spawn
│ │ │ └── analysis.rs # [! планируется] идентификация задач, анализ зависимостей, обнаружение конфликтов ресурсов
│ │ └── typecheck/
│ │ └── ...
│
├── middle/
│ ├── core/
│ │ ├── ir.rs # Определение IR (общее для VM и LLVM)
│ │ └── ir_gen.rs # Генерация IR
│ └── passes/
│ ├── codegen/
│ │ ├── mod.rs # Слой оркестрации (сейчас выводит BytecodeFile)
│ │ ├── translator.rs # Трансляция IR → байткод (для бэкенда VM)
│ │ ├── emitter.rs # Эмиссия байткода + обратная заливка переходов (для бэкенда VM)
│ │ ├── buffer.rs # Пул констант + буфер байткода (для бэкенда VM)
│ │ ├── bytecode.rs # Формат байткода + сериализация (для бэкенда VM)
│ │ ├── flow.rs # Распределение регистров + генерация меток + таблица символов (для бэкенда VM)
│ │ └── operand.rs # Разбор операндов (для бэкенда VM)
│ ├── lifetime/ # Анализ времени жизни/активности токенов
│ └── mono/ # Мономорфизация
│
├── backends/
│ ├── common/ # Общие значения/куча/опкоды
│ ├── interpreter/ # Древовидный интерпретатор (бэкенд VM)
│ ├── llvm/ # [! планируется] Кодогенерация бэкенда LLVM (см. список файлов ниже)
│ │ ├── mod.rs # [! планируется] Вход в бэкенд LLVM
│ │ ├── context.rs # [! планируется] Управление контекстом LLVM
│ │ ├── types.rs # [! планируется] Отображение типов (YaoXiang → LLVM IR)
│ │ ├── values.rs # [! планируется] Отображение значений
│ │ ├── func.rs # [! планируется] Трансляция функций
│ │ ├── spawn.rs # [! планируется] Развёртка spawn-блоков
│ │ ├── ffi.rs # [! планируется] Генерация кода FFI-вызовов
│ │ └── drop.rs # [! планируется] Вставка деструкторов
│ └── 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 только потребляет результаты анализа и порождает соответствующий код диспетчеризации.Текущее состояние: файлы
buffer.rs,emitter.rs,bytecode.rs,flow.rs,operand.rsв текущемmiddle/passes/codegen/обслуживают генерацию байткода для бэкенда VM (CodegenContext::generate()→BytecodeFile). Бэкенд LLVM будет реализован вbackends/llvm/, на одном уровне с бэкендом interpreter и runtime — оба потребляют один и тот жеModuleIR, но выдают разные целевые форматы (байткод или нативный код).
Поддержка платформенных ABI
| Платформа | Целевой триплет | Выходной формат | Соглашение о вызовах (по умолчанию для 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 (для промышленного выпуска). Операции с плавающей точкой в двух режимах исполнения имеют потенциальные точки расхождения:
| Сценарий | Риск | Стратегия |
|---|---|---|
| Распространение NaN | VM и LLVM могут по-разному обрабатывать знаковый бит и payload NaN | Компилятор нормализует представление NaN на уровне IR, при сравнениях NaN единообразно используется fcmp uno |
| Режим округления | LLVM по умолчанию round-to-nearest-even, VM зависит от CPU хоста | Не предоставлять нестандартные режимы округления, VM и LLVM единообразно используют RTNE |
| Деление на ноль | IEEE 754 определяет ±Inf, но некоторые платформы могут генерировать ловушку | В режиме отладки проверять деление на ноль и сообщать диагностику; в режиме release следовать IEEE 754 |
-0.0 vs +0.0 | Операции сравнения могут быть неэквивалентны | Единообразно использовать правила IEEE 754: +0.0 == -0.0 |
| Денормализованные числа | Некоторые платформы используют flush-to-zero | LLVM не включает атрибут denormal-fp-math, сохраняется полная семантика IEEE 754 |
Стратегия тестирования: реализовать набор кросс-бэкендовых тестов согласованности операций с плавающей точкой — один и тот же исходный код YaoXiang выполняется соответственно в бэкендах VM и LLVM, выходные значения сравниваются поэлементно. Эта группа тестов является обязательным шлюзом CI.
Компромиссы
Преимущества
- Производительность: AOT-компиляция в 10-100 раз быстрее интерпретации
- Единый фронтенд: VM и LLVM используют общий фронтенд, поведение полностью идентично
- Нулевые накладные расходы планирования: обычный код напрямую компилируется в последовательный машинный код, накладных расходов DAG вне spawn-блоков нет
- Статическая линковка: нет внешних зависимостей от среды исполнения, для развёртывания достаточно одного exe
- Нулевой GC: детерминированные деструкторы RAII, без пауз
- Нулевые накладные расходы FFI: оптимизация null-указателя
?T, оптимизация компоновки непрозрачных типов — стоимость FFI-вызовов эквивалентна C - Анализ на этапе компиляции: идентификация задач spawn-блоков и анализ зависимостей завершаются на этапе компиляции, во время исполнения остаётся только выполнение
Недостатки
- Сложность интеграции с LLVM: требуется глубокое понимание API inkwell и LLVM IR
- Время компиляции: AOT-компиляция медленнее, чем интерпретатор (разовая цена)
- Опыт отладки: отладка нативного кода требует поддержки символов DWARF/PDB (компилятор должен генерировать отладочную информацию)
- Инкрементальная компиляция: для крупных проектов требуется дополнительное проектирование инкрементальной компиляции
- Согласованность семантики чисел с плавающей точкой: VM и LLVM могут различаться в граничных случаях (распространение NaN, режим округления, деление на ноль) — для обеспечения согласованного поведения двух бэкендов требуется стратегия нормализации (см. §10)
Согласованность со связанными RFC
| RFC | Согласованность |
|---|---|
| RFC-024 Модель параллелизма spawn-блоков | ✅ Прямые подвыражения spawn-блока → диспетчеризация задач |
| RFC-008 Архитектура среды исполнения | ✅ Два бэкенда + статическая библиотека планировщика + структура каталогов модулей |
| RFC-009 Модель владения v9 | ✅ Токены &T/&mut T (нулевой размер), ref T (толстый указатель), ?T (Option) |
| RFC-026 Основной механизм FFI | ✅ native() → declare + marshalling, .drop → очистка RAII |
Альтернативы
| Альтернатива | Описание | Почему не выбрана |
|---|---|---|
| Только интерпретатор | AOT не нужен | Недостаточная производительность |
| Чистая статическая компиляция (без среды исполнения) | Без линковки планировщика | Spawn-блоки требуют диспетчеризации задач во время исполнения |
| Бэкенд Cranelift | Более быстрая компиляция | Производительность во время исполнения уступает LLVM, может рассматриваться как будущий опциональный бэкенд |
| Линковка внешней среды исполнения LLVM | Использование встроенной среды LLVM | Введение ненужных зависимостей |
Стратегия реализации
Разбивка по этапам
Этап 1: Базовая инфраструктура
- [ ] Добавить зависимость inkwell
- [ ] Реализовать инициализацию контекста LLVM (
context.rs) - [ ] Реализовать базовое отображение типов (
types.rs)
Этап 2: Трансляция функций
- [ ] Реализовать трансляцию объявлений функций (
func.rs) - [ ] Реализовать трансляцию базовых инструкций (арифметика, управление потоком, вызовы) (
translator.rs) - [ ] Реализовать отображение значений (
values.rs)
Этап 3: Трансляция типов владения
- [ ] Реализовать токены
&T/&mut T(нулевой размер, исчезают после компиляции) - [ ] Реализовать
ref T(толстый указатель{ i64*, T* }) - [ ] Реализовать
?T(тегированное объединение{ i1, T }) - [ ] Реализовать
List(T)({ T*, i64, i64 }) - [ ] Реализовать отслеживание семантики Move (для определения необходимости вставки деструктора)
Этап 4: Генерация кода для spawn-блоков
- [ ] Потребление результатов анализа из
spawn_placement.rs - [ ] Прямые подвыражения → генерация функций задач
- [ ] Генерация кода диспетчеризации зависимых задач
- [ ] Сериализация по конфликтам ресурсов
- [ ] Развёртка spawn for
Этап 5: Генерация кода FFI
- [ ]
native()→declare external(ffi.rs) - [ ] Marshalling аргументов / unmarshalling возвращаемых значений
- [ ] Компоновка непрозрачных типов (включая оптимизацию с одним полем)
- [ ] Оптимизация null-указателя для
?T(специально для FFI)
Этап 6: Генерация кода деструкторов
- [ ] Распознавание привязки
.drop - [ ] Вставка очистки в конце области видимости (в обратном порядке) (
drop.rs) - [ ] Очистка на пути досрочного возврата
- [ ] Очистка на пути распространения ошибки
?
Этап 7: Линковка библиотеки среды исполнения
- [ ] Реализовать функции среды исполнения
runtime_task_spawn/runtime_task_wait_allи т.д. - [ ] Линковка статической библиотеки среды исполнения
- [ ] Сквозные интеграционные тесты
Зависимости
- RFC-024 (параллелизм spawn-блоков) → вход для этапа 4
- RFC-009 v9 (владение) → вход для этапов 3 и 6
- RFC-008 (архитектура среды исполнения) → вход для этапа 7
- RFC-026 (механизм FFI) → вход для этапа 5
Связанные работы
Lazy Task Creation (1990)[^1]
| Атрибут | Описание |
|---|---|
| Организация | MIT |
| Авторы | James R. Larus, Robert H. Halstead Jr. |
| Суть | Отложенное создание подзадач, создание по требованию |
| Ценность | Теоретическая основа диспетчеризации задач в spawn-блоке по требованию |
Основная идея: вместо немедленного создания задач используется отложенное создание. Когда родительской задаче требуется значение дочерней задачи, дочерняя задача создаётся только в этот момент. Это решает проблему накладных расходов на тонкозернистые параллельные задачи[^1]. Диспетчеризация spawn-блоков YaoXiang заимствует этот подход — задачи идентифицируются на этапе компиляции, но во время исполнения распределяются в пул потоков по требованию.
Lazy Scheduling (2014)[^2]
| Атрибут | Описание |
|---|---|
| Организация | University of Maryland |
| Авторы | Tzannes, Caragea |
| Суть | Адаптивное планирование во время исполнения, без дополнительного состояния |
| Ценность | Справочник по проектированию планировщика WorkStealing в Full Runtime |
Язык SISAL[^3]
| Атрибут | Описание |
|---|---|
| Организация | Lawrence Livermore National Laboratory (LLNL) |
| Суть | Язык с единичным присваиванием, Dataflow-граф, неявный параллелизм |
| Ценность | Доказательство жизнеспособности модели Dataflow в промышленных применениях |
Ключевое отличие: параллелизм в SISAL неявный — язык имеет семантику единичного присваивания, и компилятор автоматически анализирует граф зависимостей по всей программе, чтобы определить параллелизм. Параллелизм в YaoXiang явный — пользователь отмечает параллельные области блоком spawn {}, и компилятор анализирует зависимости только внутри spawn-блока. Это позволяет избежать сложности полнопрограммного анализа SISAL, сохраняя контроль пользователя над параллельным поведением.
Mul-T — параллельный Scheme[^4]
| Атрибут | Описание |
|---|---|
| Организация | MIT |
| Суть | Конструкция Future, реализация Lazy Task Creation |
| Ценность | Справочник по конкретной реализации |
Сравнительная сводка
| Технология | Отложенное создание | Маркировка параллелизма | Область анализа | Владение |
|---|---|---|---|---|
| Lazy Task Creation[^1] | ✅ | Неявная | Вся программа | Н/П |
| Lazy Scheduling[^2] | ✅ | Неявная | Вся программа | Н/П |
| SISAL[^3] | ✅ | Неявная (единичное присваивание) | Вся программа | Н/П |
| Mul-T[^4] | ✅ | Явная (future) | Точка вызова | Н/П |
| YaoXiang | ✅ | Явная (spawn-блок) | Внутри spawn-блока | ✅ (Move + токены + ref) |
Инновация YaoXiang: маркировка параллелизма поднята с уровня «каждый вызов функции» (future) до уровня «структурированного блока» (spawn). Пользователь пишет обычный код и помещает spawn-блок в тех местах, где нужен параллелизм. Область анализа ограничена spawn-блоком, что обеспечивает эффективную компиляцию и предсказуемое поведение.
Приложения
Приложение А: Сравнение с Rust async
| Характеристика | Rust async | YaoXiang LLVM AOT |
|---|---|---|
| Артефакт компиляции | Конечный автомат + машинный код | Машинный код + метаданные задач spawn |
| Среда исполнения | tokio | Статически слинкованный планировщик (около 500KB-1MB) |
| Маркировка параллелизма | ключевые слова async/await | Блок spawn { } |
| Создание задачи | Генерация конечного автомата на этапе компиляции | Идентификация прямых подвыражений на этапе компиляции → функция задачи |
| Цветные функции | Заражение async | Нет цветных функций |
| Синхронное ожидание | .await | Автоматическое синхронное блокирование в spawn-блоке |
| Управление памятью | GC (во время исполнения) | RAII (детерминированное) |
| Механизм разделения | Arc::new() + ручной Weak | Ключевое слово ref (компилятор автоматически выбирает Rc/Arc) |
Приложение Б: Журнал проектных решений
| Решение | Определение | Дата |
|---|---|---|
| Принятие LLVM AOT | Прямой Codegen, без избыточных абстракций | 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 (производство), согласно RFC-008 | 2026-05-11 |
| Форма планировщика | Статическая библиотека, линкуемая в exe, около 500KB-1MB (зависит от платформы и функций), без GC | 2026-05-11 |
| Генерация кода FFI | Интеграция RFC-026: native() declare + marshalling | 2026-06-10 |
| Деструктор | .drop → вставка RAII-очистки, согласно 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 и планировщика
- RFC-009: Проект модели владения
- RFC-026: Основной механизм FFI
Жизненный цикл и местонахождение
| Статус | Расположение | Описание |
|---|---|---|
| Черновик | docs/design/rfc/ | Черновик автора, ожидает подачи на рассмотрение |
| На рассмотрении | docs/design/rfc/review/ | Открыто для обсуждения и отзывов сообщества |
| Принято | docs/design/rfc/accepted/ | Становится официальным проектным документом |
| Отклонено | docs/design/rfc/ | Сохраняется в каталоге RFC |
Текущий статус: Принято — согласовано с моделью параллелизма spawn-блоков RFC-024, моделью владения RFC-009 v9, механизмом FFI RFC-026
