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 — для промышленного выпуска.

Основные обязанности:

Исходный код → Фронтенд (общий) → 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Описание
Inti6464-битное целое со знаком по умолчанию
Int32i32Явное 32-битное целое (главным образом для FFI)
Floatf6464-битное число с плавающей точкой по умолчанию
Float32f32Явное 32-битное с плавающей точкой (для FFI)
Booli1Логическое значение
Chari32Unicode-кодовая позиция
String{ i8*, i64 }Указатель + длина в байтах
Void{}Пустой тип нулевого размера
&T—Токен нулевого размера, исчезает после компиляции, не порождает IR
&mut T—Токен нулевого размера, исчезает после компиляции, не порождает IR
ref T{ i64*, T* }Толстый указатель (указатель счётчика ссылок + указатель данных)
*TT*Голый указатель
[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Тип 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 Таблица трансляции инструкций ​

Ниже приведена стратегия трансляции каждого варианта перечисления 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, 1String — это { 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 Обзор семантики ​

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

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: зависит от data от задачи 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) и компоновки непрозрачных типов (§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() ​

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 LLVM-компоновка непрозрачных типов ​

Непрозрачный тип, определённый в RFC-026 §4.1:

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

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

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

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

llvm
; Оптимизированное LLVM-представление: прямое использование допускающего null указателя
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 Вставка очистки в конце области видимости ​

Пользовательский код:
{
    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 и деструктор ​

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

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

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

llvm
; режим отладки: проверка возвращаемого значения деструктора
%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. Структура артефактов компиляции ​

Артефакты компиляции включают следующие составляющие (конкретные определения структур будут уточнены на этапе реализации):

  • Машинный код: объектные файлы (.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_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 (для промышленного выпуска). Операции с плавающей точкой в двух режимах исполнения имеют потенциальные точки расхождения:

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

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


Компромиссы ​

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

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

Недостатки ​

  1. Сложность интеграции с LLVM: требуется глубокое понимание API inkwell и LLVM IR
  2. Время компиляции: AOT-компиляция медленнее, чем интерпретатор (разовая цена)
  3. Опыт отладки: отладка нативного кода требует поддержки символов DWARF/PDB (компилятор должен генерировать отладочную информацию)
  4. Инкрементальная компиляция: для крупных проектов требуется дополнительное проектирование инкрементальной компиляции
  5. Согласованность семантики чисел с плавающей точкой: 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 asyncYaoXiang 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-0242026-06-10
Область DAG-анализаВнутри spawn-блока, без перехода через границы spawn-блоков (согласно RFC-024)2026-06-05
Согласование модели владенияСогласование с RFC-009 v9: токены &T/&mut T + ключевое слово ref2026-06-10
Модель с двумя бэкендамиVM (разработка) + LLVM (производство), согласно RFC-0082026-05-11
Форма планировщикаСтатическая библиотека, линкуемая в exe, около 500KB-1MB (зависит от платформы и функций), без GC2026-05-11
Генерация кода FFIИнтеграция RFC-026: native() declare + marshalling2026-06-10
Деструктор.drop → вставка RAII-очистки, согласно RFC-026 §72026-06-10
Обработка побочных эффектовУдаление вывода @IO/@Pure, замена на типы ресурсов из RFC-0242026-06-10
Метаданные рефлексииКомпилируются в секцию .reflect exe, загрузка mmap по запросу2026-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, моделью владения RFC-009 v9, механизмом FFI RFC-026