Причина устаревания: 2026-06-16, решение по дизайну языка — лямбды/функциональные значения не захватывают внешние переменные неявно, вместо этого используется явная передача через параметры.
spawn { }выполняется в том же кадре и не затрагивает захват замыканий. Система анализа захвата из данного RFC была полностью удалена (~850 строк кода). Корректное решение для контекстных зависимостей = замыкание принимает только параметры + каррирование фиксируется в точке создания (SPEC §12.3 / RFC-009 §2.3). Подробности см. в Проектных решениях RFC-009.
RFC-023: Модель захвата замыканий
Ссылки:
- RFC-007: Унификация синтаксиса функций
- RFC-009: Модель владения v9
- RFC-011: Дизайн системы обобщений — раздел 2.4: встроенные marker trait Dup/Clone
Резюме
Настоящий RFC определяет модель захвата замыканий языка YaoXiang. Компилятор автоматически анализирует внешние переменные, на которые ссылается тело замыкания, и в зависимости от типа переменной (Dup/не-Dup) и того, является ли замыкание сбегающим (escaping), автоматически выбирает способ захвата — для типов Dup выполняется прямое копирование, для не-Dup без сбегания — заимствование, для не-Dup со сбеганием — Move. Пользователю не требуются аннотации; правила те же, что и для автоматического выбора заимствования при вызове функций.
Мотивация
Зачем это нужно?
В настоящее время захват замыканий — пустая реализация: поле env инструкции MakeClosure всегда пустое, и лямбда не может ссылаться ни на какие внешние переменные. Система токенов заимствования требует, чтобы замыкание могло захватывать токены &T (с нулевой стоимостью копирования), — это один из ключевых сценариев использования.
Текущая проблема
# Этот код в настоящее время не компилируется — лямбда не может ссылаться на 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() → не сбегает (немедленный вызов)Консервативный принцип: если невозможно определить, считается сбегающим.
Примеры
# 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:
// Текущий вариант (пустой)
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 только добавляет выразительности, не нарушая работы существующего кода.
Компромиссы
Преимущества
- Нулевые аннотации: пользователю не нужно указывать какие-либо аннотации захвата
- Единообразие с вызовами функций: правила захвата = правила автоматического заимствования при вызовах функций
- Нулевая стоимость: захват токенов Dup полностью устраняется в compile-time
- Безопасность: анализ сбегания предотвращает use-after-free
Недостатки
- Консервативность анализа сбегания: при невозможности определить считается сбегающим, что может приводить к ненужным Move
- Неявность: способ захвата не отражён в исходном коде, для отладки необходимо смотреть вывод компилятора
Альтернативы
| Альтернатива | Почему не выбрана |
|---|---|
Явное ключевое слово move в стиле Rust | Вводит новый синтаксис, увеличивает когнитивную нагрузку |
| Всегда Move | Невозможно выразить заимствование токенов с нулевой стоимостью |
| Всегда заимствование | Сбегание замыкания приведёт к висячим ссылкам |
| Ручная аннотация способа захвата пользователем | Противоречит философии дизайна «полная автоматизация компилятора» |
Стратегия реализации
Разделение на этапы
- Этап 1: Анализ захвата (только распознавание ссылок на внешние переменные, без различия способов захвата)
- Этап 2: Анализ сбегания + выбор режима
- Этап 3: Генерация IR + оптимизация ZST
- Этап 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
- RFC-007: Унификация синтаксиса функций
- RFC-009: Модель владения v9
- RFC-011: Дизайн системы обобщений — раздел 2.4: встроенный marker trait Dup/Clone
