Skip to content

Спецификация модели конкурентности ​

Статус: Официальная спецификация. Основана на RFC-024 (Модель конкурентности), RFC-009 (Модель владения), RFC-008 (Архитектура среды выполнения).

Данный документ определяет спецификацию модели конкурентности языка программирования YaoXiang, включая семантику блоков {}, примитив параллелизма spawn, взаимодействие с владением, обработку ошибок и типы ресурсов.

Ключевая идея дизайна — один примитив, одно правило:

spawn { ... }        ← единственный примитив параллелизма
прямое присваивание создаёт задачу    ← единственное правило
синхронное блокирующее ожидание результата      ← единственное поведение

Глава 1: Обзор ​

1.1 Сущность блоков {} ​

В YaoXiang {} — это вычислительная единица, управляемая зависимостями.

АтрибутОписание
Управление зависимостямиБлок при выполнении проверяет готовность всех внутренних переменных; если все готовы — выполняется немедленно, иначе блокируется в ожидании
Время выполненияОпределяется зависимостями, не зависит от «немедленного» или «отложенного» выполнения
Возвращаемое значениеИспользуйте return для явного возврата; без return по умолчанию возвращается Void
Единый синтаксисНезависимо от того, появляется ли в теле функции, инициализации переменной или после spawn, семантика одинакова
Изоляция области видимостиПеременные строго ограничены внутренней частью {}, не проникают во внешнюю область видимости
yaoxiang
// Пример управления зависимостями
x = compute_x()        // x готов
y = compute_y()        // y готов
result = {
    // Зависит от x и y; выполняется сразу, как только оба готовы
    return x + y
}

1.2 Правила возврата ​

ЗаписьВозвращаемое значениеОписание
= expr (без фигурных скобок)Возвращает expr напрямуюВыражение есть значение
= { ... } (с фигурными скобками)Необходимо использовать return, иначе возвращается VoidБлок требует явного возврата
yaoxiang
// Без фигурных скобок: прямой возврат
add: (a: Int, b: Int) -> Int = a + b

// С фигурными скобками: необходимо использовать return
process: (data: Data) -> Result = {
    validated = validate(data)?
    return ok(transform(validated))
}

// С фигурными скобками, но без return: возвращается Void
log: (message: String) -> Void = {
    print(message)  // Без return, возвращается Void
}

1.3 Семантика блока spawn ​

spawn { ... } — это единственный примитив параллелизма в YaoXiang.

Основные правила:

  • Прямые дочерние присваивания в блоке spawn создают параллельные задачи
  • Присваивания во вложенных {} не считаются отдельными задачами
  • Весь блок spawn блокирует выполнение синхронно, ожидая завершения всех задач перед возвратом результата
  • Нет обратных вызовов, await или аннотаций
yaoxiang
// Две задачи выполняются параллельно
(a, b) = spawn {
    fetch("url1"),      // Задача 1
    fetch("url2")       // Задача 2
}
// Продолжение после завершения обеих

1.4 Модель мышления пользователя ​

Ваш обычный код выполняется последовательно. Когда вы хотите, чтобы несколько вещей выполнялось одновременно, поместите их в блок spawn { ... }. Каждое прямое присваивание в блоке немедленно запускается (параллельно), и нужный вам результат будет автоматически ожидаться. Весь блок ждёт завершения всех задач, а затем возвращает вам конечный результат. Никаких обратных вызовов, никаких await, никаких странных аннотаций.


Глава 2: Синтаксис и семантика ​

2.1 Обычный код ​

Обычный код (вне блока spawn) выполняется последовательно.

yaoxiang
a = compute_a()     // Выполняется первым
b = compute_b(a)    // Зависит от a, выполняется после завершения a
c = compute_c(b)    // Зависит от b, выполняется после завершения b

2.2 Блок spawn ​

SpawnBlock  ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' SpawnBody '}'
SpawnBody   ::= Assignment (',' Assignment)*

Семантика:

  1. Прямые дочерние присваивания в блоке spawn выполняются как независимые задачи параллельно
  2. Результат каждой задачи связывается с соответствующей переменной шаблона
  3. Весь блок блокируется до завершения всех задач
  4. Возвращается кортеж всех результатов
yaoxiang
// Одна задача
result = spawn {
    fetch("url")
}

// Несколько задач
(a, b, c) = spawn {
    fetch("url1"),
    fetch("url2"),
    fetch("url3")
}

2.3 spawn в теле функции ​

Тело функции само по себе является блоком {}, в котором можно использовать spawn.

yaoxiang
fetch_and_parse: (urls: List(String)) -> List(Data) = {
    results = spawn for url in urls {
        parsed = parse(fetch(url))
    }
    return results
}

2.4 spawn в цикле ​

SpawnFor    ::= Identifier '=' 'spawn' 'for' Identifier 'in' Expr '{' Assignment '}'

Семантика: Параллельный по данным цикл, каждая итерация как независимая задача.

yaoxiang
// Параллельная обработка каждого элемента списка
results = spawn for item in items {
    result = process(item)
}

Примечание: Тело цикла spawn for состоит из независимых задач и не поддерживает общее изменяемое состояние между итерациями. Если требуется агрегация результатов, соберите результаты через spawn for и обработайте их снаружи.

yaoxiang
// Правильно: параллельная обработка с последующей внешней агрегацией
transformed = spawn for item in items {
    result = transform(item)
}
total = sum(transformed)   // Последовательная агрегация

2.5 Вложенный spawn ​

Блоки spawn могут быть вложенными, при этом внутренний spawn создаёт новую область конкурентности.

yaoxiang
(a, b) = spawn {
    x = spawn {
        fetch("url1"),
        fetch("url2")
    },
    y = compute(x)
}

Только прямые дочерние присваивания внутреннего spawn являются задачами; внешний spawn не проникает внутрь.


Глава 3: Взаимодействие с моделью владения ​

3.1 Семантика перемещения (Move) ​

Move — это семантика по умолчанию в YaoXiang (zero-copy). После того как переменная попадает в блок spawn, её нельзя использовать снаружи.

yaoxiang
data = load_data()
result = spawn {
    process(data)   // Право владения data перемещается в блок spawn
}
// data здесь недоступен (перемещён)

3.2 Токены заимствования ​

&T и &mut T — это доказательства разрешений нулевого размера на этапе компиляции, которые не могут пересекать границу задач. Это не специальное правило — токены являются доказательствами разрешений на этапе компиляции; для совместного использования между задачами используйте ref.

yaoxiang
data = load_data()

// Ошибка компиляции: токен заимствования не может пересекать задачу
result = spawn {
    process(&data)   // Ошибка! &T не может передаваться между задачами
}

Свойства типов токенов:

ТокенОсновная семантикаВторичные свойства
&TЗамораживает исходные данные — пока существует ReadToken, ни один WriteToken(T) не может быть полученНулевой размер, копируемый (Dup) — при гарантии заморозки множественные представления только для чтения безопасны
&mut TИсключительное чтение-запись — пока существует WriteToken, никакой другой токен (для чтения или записи) не может сосуществоватьНулевой размер, линейный (не Dup) — копирование при исключительном доступе бессмысленно

Причинно-следственная связь: Возможность дублирования ReadToken является следствием гарантии заморозки, а не наоборот. Данные заморожены (мутация невозможна) → множественные представления только для чтения безопасны → можно реализовать Dup. Если принять Dup за определение, а проверку конфликтов за заплатку, причинно-следственная связь нарушается.

3.3 Совместное использование через ref ​

ref — это единственный способ совместного использования между областями видимости. Компилятор автоматически выбирает Rc (одна задача) или Arc (между задачами), пользователю не нужно об этом заботиться.

yaoxiang
data = load_data()
shared = ref data       // Компилятор автоматически выберет Rc или Arc

result = spawn {
    process_a(shared),  // Общая ссылка
    process_b(shared)   // Общая ссылка
}

Стратегия выбора компилятора:

УсловиеВыборПричина
По умолчанию (безопасность не доказана)ArcБезопасность прежде всего, чтобы избежать гонки данных
Компилятор может доказать, что данные используются только в одной задачеRcОтсутствие накладных расходов на атомарные операции

ref vs токены заимствования:

&T / &mut Tref
ДействиеПосмотреть/изменить на местеСовместное владение
СтоимостьНулевые накладные расходы (тип нулевого размера)Rc или Arc (выбирает компилятор)
Между задачамиНевозможноВозможно (компилятор автоматически выбирает Arc)

3.4 Замыкания и захват ​

Замыкания не захватывают внешние переменные неявно (Решение RFC-009 от 2026-06-16, SPEC §12.3). Lambda использует только явные параметры и собственные локальные переменные; если нужны внешние данные, передавайте их через явные параметры или фиксируйте через каррирование в точке создания. Код ошибки компиляции для неявного захвата — E1001.

Почему запрещено: внешняя область видимости в точке определения замыкания может уже не существовать после его экраута; ссылки, захваченные неявно, не могут гарантировать своё существование. Значения, зафиксированные через каррирование, берутся в точке создания (область видимости вызывающей стороны существует), что безопасно и не несёт скрытых издержек.

yaoxiang
data = load_data()
fn = (x: Int) -> Int = data.value + x   // ❌ Ошибка компиляции E1001: неявный захват data
yaoxiang
// ✅ Правильный способ 1: явный параметр
add: (data: Data, x: Int) -> Int = data.value + x

// ✅ Правильный способ 2: фиксация через каррирование (контекст фиксируется в точке создания, замыкание принимает только параметры)
mk: (data: Data) -> (x: Int) -> Int = (data) => (x) => data.value + x

Совместное использование внутри spawn: когда требуется совместное использование между задачами, используйте ref для явного совместного владения (RFC-024):

yaoxiang
data = load_data()
shared = ref data

result = spawn {
    ((x: Int) -> Int = shared.value + x)(1),
    ((x: Int) -> Int = shared.value + x)(2)
}

spawn while запрещает захват внешних переменных типа &mut (Решение RFC-024 от 2026-07-04, ошибка на этапе компиляции, позволяет избежать гонки данных и не вводит Sync).


Глава 4: Обработка ошибок ​

4.1 Оператор ? ​

Оператор ? используется для явного распространения ошибок, аналогично семантике Rust.

yaoxiang
read_file: (path: FilePath) -> Result(String, IoError) = {
    content = open(path)?      // При ошибке немедленно распространяет её
    return content.read_all()
}

4.2 Распространение ошибок в блоке spawn ​

Правила:

  1. Ожидание завершения всех задач (даже если некоторые из них уже завершились с ошибкой)
  2. Распространение первой встреченной ошибки
  3. Использование ? для явной отметки точки распространения ошибки
yaoxiang
(a, b) = spawn {
    fetch("url1")?,     // Может завершиться ошибкой
    fetch("url2")?      // Может завершиться ошибкой
}
// Если какая-либо задача завершилась с ошибкой, весь блок spawn распространяет первую ошибку

4.3 Типы ошибок ​

Автоматическая генерация: Компилятор автоматически генерирует объединённый тип ошибок.

yaoxiang
// Компилятор выводит тип ошибки как HttpError | IoError
(a, b) = spawn {
    fetch("url"),           // Может выбросить HttpError
    read_file("data.txt")  // Может выбросить IoError
}

Ручное переопределение: Пользователь может вручную определить единый тип ошибки.

yaoxiang
AppError: Type = {
    Http: (http_error: HttpError) -> AppError,
    Io: (io_error: IoError) -> AppError,
    Parse: (parse_error: ParseError) -> AppError
}

process: (url: String, path: FilePath) -> Result(Data, AppError) = {
    (a, b) = spawn {
        fetch(url).map_err(AppError.Http)?,
        read_file(path).map_err(AppError.Io)?
    }
    return parse(a + b).map_err(AppError.Parse)?
}

Глава 5: Типы ресурсов и побочные эффекты ​

5.1 Встроенные типы ресурсов ​

Тип ресурсаОписаниеПоведение компилятора
FilePathПуть в файловой системеОперации с одним путём автоматически сериализуются
HttpUrlКонечная точка HTTPОперации с одним URL автоматически сериализуются
DBUrlСоединение с базой данныхОперации с одним соединением автоматически сериализуются
ConsoleСтандартный выводВсе операции с Console автоматически сериализуются
yaoxiang
// Операции с одним файлом автоматически сериализуются
(a, b) = spawn {
    read_file("data.txt"),      // Выполняется первой
    write_file("data.txt", x)   // Ожидает завершения чтения
}

5.2 Пользовательские типы ресурсов ​

Пользовательские типы ресурсов должны быть явно помечены.

yaoxiang
Database: Type = {
    connection_string: String,
    query: (db: Database, sql: String) -> Result(Rows, DbError)
}

5.3 Отслеживание побочных эффектов ​

Компилятор отслеживает использование типов ресурсов для обеспечения безопасности параллелизма.

yaoxiang
// Предупреждение компилятора: операции Console могут чередоваться
spawn {
    print("Hello"),     // Может чередоваться со следующей строкой
    print("World")
}

// Правильно: явная сериализация
spawn {
    print("Hello\nWorld")
}

Глава 6: Поведение компилятора ​

6.1 DAG-анализ ​

Компилятор на этапе компиляции анализирует зависимости (DAG) внутри блока spawn, определяя:

  1. Какие выражения могут выполняться параллельно
  2. Какие должны выполняться последовательно
  3. Как распределять задачи
yaoxiang
(a, b, c) = spawn {
    x = fetch("url1"),      // Задача 1
    y = fetch("url2"),      // Задача 2 (параллельно с задачей 1)
    z = process(x, y)       // Задача 3 (зависит от x и y, должна ждать)
}

6.2 Выбор Rc/Arc (консервативная стратегия) ​

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

  • По умолчанию Arc: Когда компилятор не может определить, что ref используется только в одной задаче, консервативно выбирается Arc
  • Понижение до Rc: Только когда компилятор может доказать через DAG-анализ, что данные никогда не будут разделены между задачами, происходит понижение до Rc
  • Лучше медленнее, чем с ошибкой: Дополнительные расходы от выбора Arc намного меньше, чем риск гонки данных

6.3 Предупреждение об отсутствии параллелизма ​

Если задачи в блоке spawn не имеют реальной возможности параллельного выполнения, компилятор выдаёт предупреждение.

yaoxiang
// Предупреждение компилятора: нет возможности параллельного выполнения
result = spawn {
    a = fetch("url")    // Единственная задача
}
// Рекомендация: используйте обычный код
result = fetch("url")

6.4 Обнаружение конфликтов ресурсов ​

Компилятор обнаруживает потенциальные конфликты типов ресурсов.

yaoxiang
// Ошибка компиляции: параллельная запись в один файл
spawn {
    write_file("data.txt", "a"),
    write_file("data.txt", "b")  // Ошибка!
}

Глава 7: Уровни среды выполнения ​

Этап компиляции полностью идентичен, отличие только в способе выполнения в среде выполнения (RFC-008).

УровеньПоддержка spawnDAG-анализСценарии использования
Embedded Runtime❌НетWASM, игровые скрипты, движки правил
Standard Runtime✅Внутри блока spawnВеб-сервисы, конвейеры данных
Full Runtime✅Внутри блока spawn + work stealingНаучные вычисления, массовый параллелизм

Embedded Runtime: Мгновенный исполнитель, без поддержки spawn, высокая производительность и низкое потребление.

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

Full Runtime: Standard + балансировка нагрузки WorkStealer.


Приложение: Краткий справочник по синтаксису ​

A.1 Оператор spawn ​

SpawnBlock  ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' SpawnBody '}'
SpawnFor    ::= Identifier '=' 'spawn' 'for' Identifier 'in' Expr '{' Assignment '}'
SpawnStmt   ::= SpawnBlock | SpawnFor
SpawnBody   ::= Assignment (',' Assignment)*

A.2 Обработка ошибок ​

Expr '?'              // Распространение ошибки (тип Result)

A.3 Выражение ref ​

RefExpr     ::= 'ref' Expr

A.4 Маркировка типа ресурса ​

ResourceDecl ::= Identifier ':' 'Type' '=' RecordType