Skip to content

Причина устаревания: 2026-06-16, решение по дизайну языка — лямбды/функциональные значения не захватывают внешние переменные неявно, вместо этого используется явная передача через параметры. spawn { } выполняется в том же кадре и не затрагивает захват замыканий. Система анализа захвата из данного RFC была полностью удалена (~850 строк кода). Корректное решение для контекстных зависимостей = замыкание принимает только параметры + каррирование фиксируется в точке создания (SPEC §12.3 / RFC-009 §2.3). Подробности см. в Проектных решениях RFC-009.

RFC-023: Модель захвата замыканий ​

Ссылки:

Резюме ​

Настоящий RFC определяет модель захвата замыканий языка YaoXiang. Компилятор автоматически анализирует внешние переменные, на которые ссылается тело замыкания, и в зависимости от типа переменной (Dup/не-Dup) и того, является ли замыкание сбегающим (escaping), автоматически выбирает способ захвата — для типов Dup выполняется прямое копирование, для не-Dup без сбегания — заимствование, для не-Dup со сбеганием — Move. Пользователю не требуются аннотации; правила те же, что и для автоматического выбора заимствования при вызове функций.

Мотивация ​

Зачем это нужно? ​

В настоящее время захват замыканий — пустая реализация: поле env инструкции MakeClosure всегда пустое, и лямбда не может ссылаться ни на какие внешние переменные. Система токенов заимствования требует, чтобы замыкание могло захватывать токены &T (с нулевой стоимостью копирования), — это один из ключевых сценариев использования.

Текущая проблема ​

yaoxiang
# Этот код в настоящее время не компилируется — лямбда не может ссылаться на threshold
filter_by: (items: List(Point), threshold: &Float) -> List(Point) = {
    items.filter(|p| p.x > threshold)  # ❌ threshold невозможно захватить
}

Предложение ​

Основной дизайн ​

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

Тип переменной    Сбегание замыкания    Способ захвата
─────────────────────────────────────────
Dup                любое                 Копирование (побитовое либо с нулевой стоимостью)
не-Dup             не сбегает            Автоматическое заимствование (&T или &mut T)
не-Dup             сбегает               Move (передача владения)

Определение сбегания:

spawn { || ... }           → сбегает
return || ...              → сбегает
let x = || ... ;  x сохранён в поле → сбегает
items.filter(|p| ...)      → не сбегает (синхронный вызов функции высшего порядка)
||.method()                → не сбегает (немедленный вызов)

Консервативный принцип: если невозможно определить, считается сбегающим.

Примеры ​

yaoxiang
# 1. Токены Dup — прямое копирование (нулевая стоимость)
filter_by: (items: List(Point), threshold: &Float) -> List(Point) = {
    # threshold: &Float → Dup → компилятор копирует токен в замыкание
    # Токен нулевого размера, нулевые накладные расходы во время выполнения
    items.filter(|p| p.x > threshold)
}

# 2. не-Dup + не сбегает — автоматическое заимствование
process: (buf: Buffer) -> Void = {
    # buf не Dup, filter не сбегает → автоматически создаётся токен &Buffer
    transform(|b| b.read())
    # После возврата замыкания токен освобождается, buf снова доступен
}

# 3. Замыкание сбегает — Move
spawn_worker: (data: Data) -> Void = {
    # data не Dup, spawn → сбегает → Move
    spawn { use(data) }
}

# 4. Смешанный захват
complex: (items: List(Point), config: &Config, buf: Buffer) -> List(Point) = {
    # config: &Config → Dup → копирование токена
    # buf: Buffer → не Dup, не сбегает → заимствование &mut Buffer
    items.filter(|p| {
        let threshold = config.get_threshold()
        buf.update(p)
        p.x > threshold
    })
}

# 5. Обнаружение конфликта заимствования
bad: (buf: Buffer) -> Void = {
    closure = |b| b.write()
    buf.read()  # ❌ buf уже заимствован замыканием, здесь конфликт
}

Изменения синтаксиса ​

Никаких изменений синтаксиса. Способ захвата определяется компилятором автоматически, пользователю не нужно указывать аннотации.

Подробный дизайн ​

Влияние на систему типов ​

Сигнатура типа лямбды остаётся неизменной: (params) -> Return. Захваченные переменные не отражаются в сигнатуре типа и обрабатываются компилятором на этапе генерации IR.

Изменения в компиляторе ​

КомпонентИзменениеОписание
capture.rs (новый)Анализ захвата + анализ сбегания + выбор режима~150 строк
expressions.rsВывод типа лямбды вызывает анализ захвата~10 строк
ir_gen.rsЗаполнение env в MakeClosure; пропуск ZST~80 строк
ir.rsВозможная корректировка типа env в MakeClosure~5 строк

Процесс анализа захвата:

1. Обход AST тела лямбды
2. Сбор всех ссылок Expr::Var(name)
3. Фильтрация: оставляем только переменные из внешней области видимости замыкания
4. Классификация: Read (только чтение) / Write (чтение-запись) / Move (передача)
5. Проверка атрибута типа: является ли Dup
6. Определение сбегания: способ использования замыкания
7. Выбор режима захвата:
   Dup → Copy
   не-Dup + не сбегает + Read → Borrow (&T)
   не-Dup + не сбегает + Write → BorrowMut (&mut T)
   не-Dup + сбегает → Move

Генерация IR:

rust
// Текущий вариант (пустой)
Instruction::MakeClosure { dst, func, env: Vec::new() }

// Изменённый вариант
Instruction::MakeClosure { dst, func, env: captured_env }

// Логика генерации captured_env:
for captured in captures {
    match captured.mode {
        Copy if is_zst(captured.ty) => {
            // Тип нулевого размера — не генерируется никаких инструкций
            // Тело замыкания напрямую ссылается на внешнюю переменную (устраняется в compile-time)
        }
        Copy => {
            // Генерация Move dst, src (поверхностное копирование типа Dup)
        }
        Borrow => {
            // Генерация Borrow dst, src (создание ReadToken)
        }
        BorrowMut => {
            // Генерация Borrow dst, src (создание WriteToken)
        }
        Move => {
            // Генерация Move dst, src (передача владения)
        }
    }
}

Поведение во время выполнения ​

Способ захвата не влияет на производительность во время выполнения:

  • Dup + ZST (например, токен &T) → ноль инструкций, тело замыкания напрямую ссылается на внешнюю переменную
  • Dup + не-ZST (например, Int) → однократное копирование через регистр
  • Borrow/BorrowMut → создание токена (концепция compile-time, нулевые накладные расходы)
  • Move → та же стоимость, что и обычный Move

Обратная совместимость ​

Полностью совместимо. В настоящее время все лямбды не могут захватывать внешние переменные, поэтому данный RFC только добавляет выразительности, не нарушая работы существующего кода.

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

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

  1. Нулевые аннотации: пользователю не нужно указывать какие-либо аннотации захвата
  2. Единообразие с вызовами функций: правила захвата = правила автоматического заимствования при вызовах функций
  3. Нулевая стоимость: захват токенов Dup полностью устраняется в compile-time
  4. Безопасность: анализ сбегания предотвращает use-after-free

Недостатки ​

  1. Консервативность анализа сбегания: при невозможности определить считается сбегающим, что может приводить к ненужным Move
  2. Неявность: способ захвата не отражён в исходном коде, для отладки необходимо смотреть вывод компилятора

Альтернативы ​

АльтернативаПочему не выбрана
Явное ключевое слово move в стиле RustВводит новый синтаксис, увеличивает когнитивную нагрузку
Всегда MoveНевозможно выразить заимствование токенов с нулевой стоимостью
Всегда заимствованиеСбегание замыкания приведёт к висячим ссылкам
Ручная аннотация способа захвата пользователемПротиворечит философии дизайна «полная автоматизация компилятора»

Стратегия реализации ​

Разделение на этапы ​

  1. Этап 1: Анализ захвата (только распознавание ссылок на внешние переменные, без различия способов захвата)
  2. Этап 2: Анализ сбегания + выбор режима
  3. Этап 3: Генерация IR + оптимизация ZST
  4. Этап 4: Интеграция обнаружения конфликтов заимствования

Зависимости ​

  • Зависит от RFC-011 (система обобщений, раздел 2.4: trait Dup/Clone) — необходим trait Dup для определения копируемости переменной
  • Зависит от RFC-009 v9 (токены заимствования) — режимы захвата Borrow/BorrowMut требуют типов токенов
  • После реализации RFC-023 и данного RFC можно приступать к реализации системы токенов заимствования (реализация RFC-009 v9)

Риски ​

  • Анализ сбегания может оказаться слишком консервативным, приводя к ненужным Move; возможна последующая оптимизация
  • Анализ захвата для обобщённых замыканий может потребовать дополнительной обработки

Запись проектных решений ​

РешениеВыборПричинаДата
Выбор способа захватаПолная автоматизацияЕдинообразие с правилами вызова функций2026-05-29
Анализ сбеганияКонсервативный принципЕсли невозможно определить — считается сбегающим, приоритет безопасности2026-05-29
Оптимизация ZSTПропуск на этапе генерации IRПроще, чем последующий оптимизирующий проход2026-05-29
Захват не отражён в сигнатуре типаВнутренняя обработка компилятораСохраняет лаконичность типа лямбды2026-05-29

Ссылки ​

Официальная документация YaoXiang ​

Внешние ссылки ​