Skip to content

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Описание
Inti6464-битный знаковый целочисленный тип по умолчанию
Int32i32Явный 32-битный целочисленный тип (в основном для FFI)
Floatf6464-битный тип с плавающей запятой по умолчанию
Float32f32Явный 32-битный тип (в основном для FFI)
Booli1Булев тип
Chari32Unicode code point
String{ i8*, i64 }Указатель + длина в байтах
Void{}Пустой тип нулевого размера
&TТокен нулевого размера, исчезает после компиляции, не создаёт никакого IR
&mut TТокен нулевого размера, исчезает после компиляции, не создаёт никакого IR
ref T{ i64*, T* }Fat-указатель (указатель с подсчётом ссылок + указатель на данные)
*TT*Голый указатель
[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Тип YaoXiangLLVM IRОписание
intInt32i32
longInt64i64
floatFloat32f32
doubleFloat64f64
charChari32C char → YaoXiang Char (совместимость с Unicode)
char*String{ i8*, i64 }marshalling: C string → YaoXiang String
boolBooli1
size_tUinti64
void**Voidi8*
struct T*T (прозрачный тип)T*Передача указателя
typedef struct T TT (непрозрачный тип){ 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 (из стекового слота)
Dupstack.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.

Арифметические инструкции:

Инструкция IRLLVM 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)

Побитовые инструкции:

Инструкция IRLLVM 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Арифметический сдвиг вправо

Инструкции сравнения:

Инструкция IRLLVM 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

Инструкции управления потоком:

Инструкция IRLLVM 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Возврат без значения

Инструкции вызова:

Инструкция IRLLVM 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Оптимизация хвостового вызова

Инструкции памяти:

Инструкция IRLLVM 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 (куча)

Инструкции доступа к структурам/массивам:

Инструкция IRLLVM 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 по порядку полей

Инструкции преобразования типов:

Инструкция IRLLVM IRОписание
Cast { dst, src, target_type }bitcast / trunc / zext / sext / fptrunc / fpext / sitofp / fptosi / inttoptr / ptrtointВыбор подходящей инструкции cast по комбинации типов источника/назначения
TypeTest(val, type)Compile-time тест типа, генерирует icmp eq сравнения метки типа

Инструкции владения и заимствования:

Инструкция IRLLVM 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)Атомарное декрементирование + условное освобождение

Инструкции параллелизма:

Инструкция IRLLVM IRОписание
Spawn { closures, plan, result }Раскрывается в последовательность вызовов планировщикаПодробности в §5, runtime task_spawn + task_wait_all
YieldНа AOT-пути spawn-блоки синхронно ожидают, не нуждаются в yield; игнорируется

Инструкции unsafe-блоков и голых указателей:

Инструкция IRLLVM IRОписание
UnsafeBlockStartCompile-time маркер, не генерирует IR
UnsafeBlockEndCompile-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

Строковые инструкции:

Инструкция IRLLVM IRОписание
StringLength { dst, src }%len = extractvalue { i8*, i64 } %src, 1String — это { 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

Инструкции замыканий:

Инструкция IRLLVM 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 со стека в кучу

Прочие инструкции:

Инструкция IRLLVM 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 Обзор семантики

yaoxiang
(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

llvm
; Вход в блок 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 Зависимые задачи

yaoxiang
result = spawn {
    data = fetch("url"),       // Задача 1: без зависимостей
    processed = parse(data),   // Задача 2: зависит от данных задачи 1
    return processed
}

Компилятор обнаруживает, что parse(data) использует data, произведённую задачей 1, и при генерации кода планирования помечает зависимость:

llvm
; Задача 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:

yaoxiang
(a, b) = spawn {
    r1 = db.exec("SELECT ..."),   // Использует SqliteDb (тип ресурса)
    r2 = db.exec("INSERT ...")    // Тот же экземпляр → автоматическая сериализация
}

Компилятор обнаруживает, что один экземпляр ресурса используется двумя задачами, и генерирует последовательную зависимость:

llvm
; Задача 2 зависит от задачи 1 (автоматическая сериализация по ресурсу)
call @runtime_task_spawn_with_dep(%tasks[1], %task2_fn, %tasks[0])

5.6 spawn for для данных параллелизма

yaoxiang
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()

yaoxiang
sqlite3_open: (filename: String) -> SqliteDb = native("sqlite3_open")

Компилируется в LLVM IR:

llvm
; Объявление внешней 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:

yaoxiang
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:

yaoxiang
sqlite3_open: (filename: String) -> ?SqliteDb = native("sqlite3_open")

Универсальное LLVM-представление: { i1, { i8* } } — флаг наличия + данные.

Оптимизация для FFI null-указателей: Если ?T имеет T как непрозрачный тип (внутренне указатель), компилятор использует оптимизацию null-указатель = None:

llvm
; Оптимизированное LLVM-представление: прямое использование nullable-указателя
define i8* @__yx_sqlite3_open(...) {
    %raw = call i8* @sqlite3_open(...)
    ; null → None, не-null → Some (обёрнут в непрозрачный тип)
    ret i8* %raw
}

Вызывающая сторона:

llvm
%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

yaoxiang
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 и деструктуризация

yaoxiang
db = SqliteDb.open("test.db")
db2 = db                // Move: владение передаётся db2
// db уже недействителен, drop для db не вставляется
// ← Конец области видимости: drop вставляется только для db2

Компилятор отслеживает семантику Move (RFC-009 §1) и вставляет вызов деструктора только в месте финального владельца переменной.

7.4 Обработка ошибок деструктора

llvm
; Режим 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 void

8. Структура артефактов компиляции

Артефакты компиляции содержат следующие компоненты (конкретные определения 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_64x86_64-unknown-linux-gnuELFSystem V AMD64
macOS x86_64x86_64-apple-darwinMach-OSystem V AMD64
macOS ARM64aarch64-apple-darwinMach-OARM64 AAPCS
Windows x86_64x86_64-pc-windows-msvcCOFFMicrosoft x64

FFI-вызовы по умолчанию используют платформенное C-соглашение о вызовах. Пользователь может переопределить через опции типа native("symbol", cc = "stdcall") (в соответствии с будущими расширениями RFC-026).

Согласованность семантики чисел с плавающей запятой (VM ↔ LLVM)

Ключевое обещание архитектуры с двумя бэкендами — согласованность поведения VM (разработка и отладка) и LLVM (production). Операции с плавающей запятой имеют потенциальные точки несоответствия в двух режимах выполнения:

СценарийРискСтратегия
Распространение NaNVM и 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-zeroLLVM не включает атрибут denormal-fp-math, сохраняет полную семантику IEEE 754

Стратегия тестирования: Реализовать набор тестов согласованности floating-point для всех бэкендов — один и тот же исходный код YaoXiang выполняется соответственно на VM и LLVM бэкендах, выходные данные сравниваются поэлементно. Эти тесты являются обязательным gate в CI.


Компромиссы

Преимущества

  1. Производительность: AOT-компиляция быстрее интерпретируемого выполнения в 10-100x
  2. Унифицированный передний план: VM и LLVM совместно используют один и тот же передний план, поведение полностью согласовано
  3. Нулевые накладные расходы планирования: Обычный код напрямую генерирует последовательный машинный код, вне блоков spawn нет накладных расходов DAG
  4. Статическая линковка: Нет зависимостей от внешнего runtime, один exe для развёртывания
  5. Нулевой GC: RAII детерминированная деструктуризация, без пауз
  6. FFI нулевые накладные расходы: Оптимизация null-указателей ?T, оптимизация layout непрозрачных типов, стоимость FFI-вызовов эквивалентна C
  7. Compile-time анализ: Распознавание задач блока spawn и анализ зависимостей выполняются на этапе компиляции, runtime только выполняет

Недостатки

  1. Сложность интеграции LLVM: Требуется глубокое понимание inkwell API и LLVM IR
  2. Время компиляции: AOT-компиляция медленнее интерпретатора (разовый компромисс)
  3. Опыт отладки: Отладка нативного кода требует поддержки символов DWARF/PDB (компилятор должен генерировать отладочную информацию)
  4. Инкрементная компиляция: Инкрементная компиляция крупных проектов требует дополнительного проектирования
  5. Согласованность семантики 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 базовый механизм FFInative() → 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 asyncYaoXiang LLVM AOT
Артефакты компиляцииState machine + машинный кодМашинный код + spawn-метаданные задач
RuntimetokioСтатически линкованный планировщик (≈ 500KB-1MB)
Маркер параллелизмаasync/await ключевые словаБлоки spawn { }
Создание задачState machine генерируется compile-timeРаспознавание прямых подвыражений compile-time → функции задач
"Цветные" функцииasync заразностьНет окрашивания функций
Синхронное ожидание.awaitАвтоматическая синхронная блокировка блока spawn
Управление памятьюGC (runtime)RAII (детерминированное)
Механизм совместного использованияArc::new() + ручной Weakref ключевое слово (компилятор автоматически выбирает Rc/Arc)

Приложение B: Журнал решений по дизайну

РешениеРешениеДата
Принятие LLVM AOTПрямая кодогенерация, без чрезмерных абстракций2026-02-15
Согласование модели параллелизмаСогласование с моделью прямых подвыражений блока spawn из RFC-0242026-06-10
Область анализа DAGВнутри блока spawn, не пересекая блоки spawn (согласование с RFC-024)2026-06-05
Согласование модели владенияСогласование с RFC-009 v9: токены &T/&mut T + ключевое слово ref2026-06-10
Модель двойного бэкендаVM (разработка) + LLVM (production), согласование с RFC-0082026-05-11
Форма планировщикаСтатическая библиотека, линкуется в exe, ≈ 500KB-1MB (зависит от платформы и возможностей), без GC2026-05-11
Генерация FFI-кодаИнтеграция RFC-026: объявления native() + marshalling2026-06-10
Деструкторы.drop → вставка RAII cleanup, согласование с RFC-026 §72026-06-10
Обработка побочных эффектовУдаление вывода @IO/@Pure, переход на типы ресурсов из RFC-0242026-06-10
Рефлексивные метаданныеКомпилируются в сегмент .reflect exe, загрузка по требованию через mmap2026-05-11
Цитирование статейСохранение Lazy Task Creation и др., чёткое указание отличий YaoXiang2026-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.


Жизненный цикл и судьба

СтатусРасположениеОписание
Черновикdocs/design/rfc/Авторский черновик, ожидает подачи на рецензирование
На рецензииdocs/design/rfc/review/Открытое обсуждение сообщества и обратная связь
Принятоdocs/design/rfc/accepted/Становится официальным документом дизайна
Отклоненоdocs/design/rfc/Сохраняется в каталоге RFC

Текущий статус: Принято — согласовано с моделью параллелизма блоков spawn из RFC-024, моделью владения v9 из RFC-009, механизмом FFI из RFC-026