Skip to content

RFC-008: Архитектура Runtime с разделением модели конкурентности и планировщика

⚠️ Указание о согласовании: Данный документ согласован с RFC-024 Новая модель конкурентности. Старый анализ DAG всей программы, аннотации @block/@eager, модель уровней L1/L2/L3 заменены примитивом параллельных блоков spawn {}. Анализ DAG теперь применяется только внутри блоков spawn {}.

Справочные материалы:

Аннотация

Данный документ определяет ключевой дизайн архитектуры Runtime:

  1. Трёхуровневая архитектура Runtime: Embedded (немедленное выполнение) → Standard (spawn + DAG планирование) → Full (WorkStealing)
  2. Разделение компиляции и выполнения: Этап компиляции полностью идентичен, различается только способ выполнения в runtime
  3. Двухбэкендная модель: VM (разработка и отладка) и LLVM AOT (производство), полностью идентичное поведение
  4. Планировщик = статическая библиотека: При AOT компиляции планировщик линкуется в exe, ~200-500KB, без GC
  5. Синхронность — частный случай планирования: num_workers=1 означает синхронный режим

Ключевое уточнение: это не Java

Java:   .java → .class → JVM (интерпретация/JIT + GC)       ← всегда требуется виртуальная машина
YaoXiang разработка: .yx → IR → выполнение в VM (быстрая итерация, пошаговая отладка)
YaoXiang производство: .yx → IR → LLVM → нативный exe (планировщик линкуется внутрь)

VM — инструмент разработки, а не сущность runtime. Как в Go: go run vs go build.
Итоговый exe = ваш нативный код + статическая библиотека планировщика + метаданные рефлексии.
Никакого интерпретатора, никакого JIT, никакого GC.

Мотивация

Основное противоречие

ПротиворечиеОписание
Прозрачность vs контрольБлоки spawn предоставляют явное управление конкурентностью, обычный код выполняется последовательно
Ядро vs опциональностьspawn — это основной примитив параллелизма, WorkStealing — продвинутая возможность при num_workers>1
Однопоточность vs конкурентностьВ однопоточном режиме конкурентность проявляется как асинхронность, синхронность — лишь частный случай планирования

Предложение

1. Трёхуровневая архитектура Runtime

┌──────────────────────────────────────────────────────────────────┐
│                    Этап компиляции (одинаков для всех режимов)    │
│                                                                  │
│  Исходный код → Lexer → Parser → TypeCheck → Codegen → IR       │
│                                                                  │
│  ⚠️ Единая система разбора, проверки типов, кодогенерации, IR    │
└──────────────────────────────────────────────────────────────────┘

          ┌────────────────────┼────────────────────┐
          ▼                    ▼                    ▼
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ 🟢 Embedded      │ │ 🔵 Standard   │ │ 🟣 Full          │
│ Немедленный      │ │ spawn + DAG   │ │ Полный планировщик│
│ исполнитель      │ │ Конкурентность│ │ Параллельная     │
│ Синхронное       │ │ внутри блоков │ │ оптимизация      │
│ выполнение       │ │ spawn         │ │ WorkStealing     │
│ Без поддержки    │ │ Автоматическая│ │                  │
│ spawn            │ │ конкурентность│ │                  │
└──────────────────┘ └───────────────┘ └──────────────────┘
ЭтапEmbeddedStandardFull
Компиляцияодинаковаодинаковаодинакова
Режимсинхронныйконкурентностьпараллельный
выполненияв блоках spawn
Использованиенизкоесреднеевысокое
памяти
Конкурентностьнетвнутри блоковвнутри блоков +
spawnпараллелизм
Поддержка spawn
Анализ DAGнетвнутри блоковвнутри блоков
spawnspawn
WorkStealerнетнет

Embedded Runtime: Целевые платформы — WASM/скрипты игр/движки правил. Немедленный исполнитель без поддержки spawn, высокопроизводительный с малым потреблением памяти.

Standard Runtime: Целевые платформы — веб-сервисы/конвейеры данных. Поддержка блоков spawn {} с анализом DAG и автоматической конкурентностью внутри блоков spawn. При num_workers=1 — однопоточный асинхронный режим.

Full Runtime: Целевые платформы — научные вычисления/масштабный параллелизм. Standard + WorkStealing балансировка нагрузки.

2. Разделение планировщика: обобщения + внедрение

Главный принцип: VM не зависит напрямую от конкретного планировщика, использует его через обобщённый параметр [S].

yaoxiang
# Определение интерфейса планировщика
Scheduler: Type = {
    spawn: (Task) -> TaskId,
    await: (TaskId) -> Result,
    spawn_with_deps: (Task, List(TaskId)) -> TaskId,
    await_all: (List(TaskId)) -> List(Result),
    stats: () -> SchedulerStats,
}

# Однопоточный планировщик
SingleThreadScheduler: Scheduler = {
    spawn: (task) => { task_queue.push(task); generate_task_id() },
    await: (task_id) => { ... },
    spawn_with_deps: (task, deps) => { ... },
    await_all: (task_ids) => { ... },
    stats: () => { queue_size: task_queue.len() },
}

# Многопоточный планировщик
MultiThreadScheduler: Scheduler = {
    spawn: (task) => { work_queue.push(task); generate_task_id() },
    await: (task_id) => { wait_for_completion(task_id) },
    spawn_with_deps: (task, deps) => { ... },
    await_all: (task_ids) => { ... },
    stats: () => { workers: get_worker_stats() },
}

# VM использует планировщик через обобщения
create_vm: [S: Scheduler](scheduler: S) -> VM = (scheduler) => {
    VM(scheduler: scheduler, memory: create_memory(), dag: create_dag())
}

Ключевые моменты:

  • Полиморфизм на этапе компиляции, нулевые накладные расходы в runtime
  • Не требуются объекты-типажи
  • Обобщённое ограничение типа [S: Scheduler] уже определено в RFC-011

3. Синхронность = частный случай планирования

❌ Заблуждение: отключить планировщик
✅ Правильно: использовать планировщик с одним worker

num_workers = 1 → однопоточный асинхронный планировщик
num_workers > 1 → многопоточный параллельный планировщик

Тот же интерфейс планировщика, только разная конфигурация. Устраняем особые случаи.

4. Место DAG

Важное изменение: Анализ DAG больше не применяется ко всей программе, а только внутри блоков spawn {}. Обычный код (вне блоков spawn) выполняется последовательно, анализ DAG не требуется.

УровеньПоддержка spawnОбласть анализа DAGОписание
Core Runtimeвнутри блоков spawnЯдро конкурентности
Standard Runtimeвнутри блоков spawnspawn + DAG планирование
Embedded RuntimeнетНемедленное выполнение, без конкурентности

5. Модель выполнения снизу вверх (внутри блоков spawn)

Важное изменение: Анализ DAG снизу вверх выполняется только внутри блоков spawn {}, больше не применяется ко всей программе.

Пользовательский код (конкурентность внутри блока spawn):
    (a, b) = spawn {
        fetch(url0),
        fetch(url1)
    }
    print(a)

Компиляционный анализ (снизу вверх внутри блока spawn):
    fetch(url0) и fetch(url1) не имеют взаимозависимостей → можно выполнять параллельно
    print(a) вне блока spawn → последовательное выполнение, ожидание завершения spawn

Runtime планирование (от листьев внутри блока spawn):
    fetch(url0) ┐
                ├→ параллельное выполнение
    fetch(url1) ┘
    print(a)                       ← вне блока spawn, последовательное выполнение

Ключевые моменты:

  • Анализ зависимостей снизу вверх ограничен только блоками spawn {}
  • Задачи без зависимостей внутри блока spawn выполняются параллельно
  • Код вне блока spawn выполняется последовательно, ожидая завершения блока spawn

6. Модель компиляции: два бэкенда + статически линкуемый Runtime

6.1 Два бэкенда, одно поведение

                      ┌─────────────────────┐
                      │  Компиляционный     │
                      │  фронтенд (единый)   │
                      │  Lexer → Parser     │
                      │  → TypeCheck        │
                      │  → анализ DAG       │
                      │  внутри блоков      │
                      │  spawn              │
                      │  → анализ побега    │
                      │  → обнаружение      │
                      │  циклов             │
                      └──────────┬──────────┘

                    ┌────────────┴────────────┐
                    ▼                         ▼
        ┌───────────────────┐     ┌───────────────────┐
        │  VM бэкенд        │     │  LLVM бэкенд      │
        │  (разработка)     │     │  (производство)   │
        │                   │     │                   │
        │  Генерация IR/    │     │  Генерация        │
        │  байткода         │     │  нативного кода   │
        │  VM интерпретирует│     │  Линковка рантайм │
        │  Поддержка        │     │  статической      │
        │  пошаговой        │     │  библиотеки       │
        │  отладки          │     │  Вывод .exe       │
        │  Быстрая          │     │  Нулевые наклад-  │
        │  итерация         │     │  ные расходы      │
        └───────────────────┘     │  интерпретации    │
                 │                 └───────────────────┘
                 ▼                         │
           Полностью                       ▼
           идентичное            Полностью идентичное
           поведение             поведение

VM бэкенд: Используется при разработке. Изменил код → сразу запустил → пошаговая отладка → быстрая итерация. Поведение полностью идентично финальному exe.

LLVM бэкенд: Используется при публикации. AOT компиляция в нативный код, планировщик линкуется как статическая библиотека. Никакого интерпретатора, никакого JIT.

6.2 Планировщик = статическая библиотека, не виртуальная машина

Внутренняя структура итогового exe:

┌────────────────────────────────────────────┐
│  Ваш код (нативный машинный код)            │
│  ├── План выполнения DAG, определённый      │
│  │   на этапе компиляции                    │
│  ├── Встроенные операции Move/ref/clone     │
│  └── Код освобождения RAII                 │
├────────────────────────────────────────────┤
│  Статическая библиотека Runtime (~200-500KB)│
│  ├── Пул потоков (фиксированный размер =    │
│  │   num_workers)                          │
│  ├── Цикл событий (libuv / io_uring)       │
│  ├── Очереди WorkStealing (только Full      │
│  │   Runtime)                              │
│  ├── Аллокатор памяти (jemalloc /          │
│  │   mimalloc)                             │
│  └── Метаданные рефлексии (загружаются по  │
│      требованию, не резидентны в памяти)   │
├────────────────────────────────────────────┤
│  Отсутствует:                               │
│  ❌ Интерпретатор байткода                  │
│  ❌ JIT компилятор                          │
│  ❌ GC                                      │
│  ❌ Виртуальная машина                      │
└────────────────────────────────────────────┘

Сравнение:

ЯзыкJavaGoYaoXiang
РезультатБайткодНативный кодНативный код
компиляции
СпособJVMПрямоеПрямое
выполненияинтерпретация/выполнениевыполнение
JIT
Размер~200MB (JVM)~1-2MB (с GC)~200-500KB (без GC)
runtime
УправлениеGCGCRAII (детерминированное)
памятью
РефлексияРезидентнаРезидентна**Хранится в exe,
в памятив памятизагружается по требованию**

6.3 Почему производительность планировщика постоянна

Ключевое понимание: Основная часть работы выполняется на этапе компиляции, в runtime происходит только "выполнение".

Этап компиляции (однократно, не в runtime):
    ├── Анализ DAG внутри блоков spawn: кто от кого зависит
    ├── Топологическая сортировка: определение порядка
    │   выполнения внутри блоков spawn
    ├── Идентификация параллельных задач: поддеревья
    │   без зависимостей внутри блоков spawn
    ├── Анализ побега: ref → Rc или Arc
    ├── Обнаружение циклов: автопонижение до Weak
    │   или ошибка
    └── Встраивание: мелкие функции просто разворачиваются

Этап выполнения (при каждом запуске, структуры данных фиксированы):
    ├── Диспетчеризация задач в пул потоков
    │   в порядке, определённом компилятором для
    │   DAG внутри блоков spawn
    ├── При I/O → приостановка текущей задачи,
    │   цикл событий берёт управление
    ├── Задача готова → возврат в очередь готовых
    └── Вот и всё.

Сам планировщик — структура данных фиксированного размера: пул потоков, цикл событий, рабочие очереди. Никакого динамического роста, никакой адаптивной реоптимизации, никакого GC-сканирования. Поведение полностью предсказуемо.

На этапе компиляции уже вычислено, "что планировать" внутри блоков spawn, runtime только "выполняет". Это отличается от tokio — tokio динамически строит цепочки Future в runtime. DAG YaoXiang статический и ограничен блоками spawn.

6.4 Рефлексия: хранится, не резидентна

Метаданные рефлексии генерируются на этапе компиляции, хранятся в отдельном сегменте (section) exe. При запуске программы не загружаются. При первом запросе рефлексии загружаются в память по требованию через mmap. Аналогично:

Структура exe:
  .text     ← ваш код
  .rodata   ← константы
  .reflect  ← метаданные рефлексии (информация о типах,
              сигнатуры функций и т.д.)
              mmap загрузка по требованию, неиспользуемое
              не занимает память

Компромисс: Увеличение размера exe (содержит данные рефлексии), но нулевые затраты памяти в runtime, если не обращаться. При первом обращении есть задержка загрузки (аналогично JIT warmup), далее нулевые накладные расходы.

src/
├── lib.rs
├── main.rs
├── backends/                          # Бэкенды runtime
│   ├── common/                        # Общие для всех бэкендов (значения, куча, опкоды)
│   │   ├── allocator.rs
│   │   ├── heap.rs
│   │   ├── opcode.rs
│   │   └── value.rs
│   ├── dev/                           # REPL + отладчик
│   │   ├── debugger.rs
│   │   ├── shell.rs
│   │   └── repl/
│   ├── interpreter/                   # 🟢 Древовидный интерпретатор (бывший Embedded/VM)
│   │   ├── ffi.rs
│   │   ├── frames.rs
│   │   ├── registers.rs
│   │   ├── runtime.rs
│   │   └── executor/
│   └── runtime/                       # 🔵 Runtime скомпилированной VM
│       ├── engine.rs
│       ├── facade.rs
│       └── task.rs
├── frontend/                          # Компиляционный фронтенд (общий для всех бэкендов)
│   ├── compiler.rs
│   ├── config.rs
│   ├── pipeline.rs
│   ├── core/
│   │   ├── lexer/
│   │   ├── parser/
│   │   ├── typecheck/
│   │   │   ├── checker.rs
│   │   │   ├── spawn_placement.rs     # ★ Анализ DAG/конкурентности внутри блоков spawn
│   │   │   │                           #   (перенесён из бывшего frontend/dag/)
│   │   │   ├── inference/
│   │   │   └── traits/
│   │   └── types/
│   ├── events/
│   ├── module/
│   └── pipeline/
├── middle/                            # Средний уровень
│   ├── core/                          # IR и байткод
│   │   ├── bytecode.rs
│   │   ├── ir.rs                      #   Определение IR (общее для VM и LLVM)
│   │   └── ir_gen.rs
│   └── passes/                        # Компиляционные проходы
│       ├── codegen/                   # Генерация кода (бывший codegen/)
│       ├── lifetime/                  # Анализ времени жизни/заимствования
│       └── mono/                      # Мономорфизация
├── lsp/                               # Language Server
├── formatter/                         # Форматтер исходного кода
├── package/                           # Менеджер пакетов
├── std/                               # Стандартная библиотека
│   ├── concurrent.rs
│   ├── io.rs
│   ├── list.rs
│   ├── math.rs
│   ├── net.rs
│   ├── string.rs
│   └── weak.rs
└── util/                              # Утилиты
    ├── diagnostic/
    ├── i18n/
    └── config/

Пояснение к сопоставлению каталогов (старый → новый):

Старый каталогНовое расположениеОписание
frontend/dag/frontend/core/typecheck/spawn_placement.rsАнализ DAG внутри блоков spawn интегрирован
в проверку типов
codegen/middle/passes/codegen/Генерация кода перенесена в средний уровень
embedded/backends/interpreter/Древовидный интерпретатор
runtime/backends/runtime/Runtime скомпилированной VM
vm/backends/interpreter/Объединён с embedded
full/(не реализовано)Full Runtime + WorkStealing, в будущих
версиях
reflect/(не реализовано)Метаданные рефлексии, в будущих версиях
core/backends/common/Общие значения/куча/опкоды

Компромиссы

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

  • Чёткое разделение слоёв: Три уровня — Embedded / Standard / Full
  • Переиспользование компиляции: Фронтенд код полностью общий
  • Разделённые обобщения: Полиморфизм на этапе компиляции, нулевые накладные расходы
  • Согласованность: Синхронность — частный случай планирования
  • Дружелюбность к встраиваемым системам: Высокая производительность + малое потребление памяти + быстрый запуск

Недостатки

  • Начальная сложность: Требуется определить интерфейс планировщика и несколько вариантов runtime
  • Компиляционная привязка: Тип планировщика определяется на этапе компиляции

Журнал решений по дизайну

РешениеРешениеДата
Схема разделенияОбобщения + внедрение2025-01-05
планировщика
ОднопоточныйСинхронность — частный случай планирования2025-01-05
режим
РеализацияDAG естественно поддерживает2025-01-05
асинхронности
WorkStealerПродвинутая возможность Full Runtime2025-01-05
Дизайн дляНемедленное выполнение, без DAG планирования2025-01-05
встраиваемых
систем
Этап компиляцииВсе рантаймы используют единый фронтенд2025-01-05
РазделениеEmbedded / Standard / Full2025-01-05
по уровням
ОграниченияОпределены в RFC-0112025-01-25
типов
ПостроениеСтатический граф зависимостей,2025-01-05
графа зависимостейопределяется на этапе компиляции
ДвухбэкенднаяVM (разработка и отладка) + LLVM AOT2026-05-11
модель(производство), идентичное поведение
ФормаСтатическая библиотека, линкуется в exe,2026-05-11
планировщика~200-500KB, без GC
МетаданныеКомпилируются в отдельный сегмент exe,2026-05-11
рефлексииmmap загрузка по требованию
ПроизводительностьАнализ DAG выполняется на этапе компиляции,2026-05-11
планировщикав runtime только выполнение
СогласованиеАнализ DAG ограничен блоками spawn,2026-06-05
области DAGсогласовано с RFC-024
ОбновлениеEmbedded без spawn, Standard поддерживает2026-06-05
трёхуровневойspawn
архитектуры

Список литературы


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

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