RFC-010a: Вычисление хвостового выражения и семантика return
Ссылки:
- RFC-010: Унифицированный синтаксис типов — модель
name: type = value, вычислительные единицы{}, управляемые зависимостями- RFC-007: Унификация синтаксиса определения функций — правила возврата из блоков кода, досрочный возврат
- RFC-038: Правила завершения операторов и переноса строк — поведение операторов и выражений при переносе строк
- Спецификация языка §Система типов — принцип взрыва
Never
Резюме
Унифицировать семантику return и вычисления блоков, устранив конфликт формулировок между RFC-007 и RFC-010.
Предлагаются три самосогласованных правила: значение блока равно его хвостовому выражению (единственный выход); return — это нелокальный выход типа Never (выход из функции, а не «возврат в блок»); if без else принимает Void.
return и вычисление блока не разделяются на уровне языка — { return n } как блок имеет значение n (тип Never), при этом return выполняет выход из функции; оба факта сосуществуют благодаря принципу взрыва (Never <: T). «Досрочный возврат» из RFC-007 и «блок имеет значение» из RFC-010 являются следствиями этих трёх правил, а не противоречащими исключениями.
Никакого нового синтаксиса, никаких новых ключевых слов.
Состояние реализации
Настоящий RFC принят. Состояние реализации трёх правил:
| Правило | Подпункт | Состояние |
|---|---|---|
| ① Значение блока = хвостовое выражение | Хвостовое выражение тела функции | ✅ Реализовано |
| ① Значение блока = хвостовое выражение | if / match в хвостовой позиции | ✅ Реализовано (#344) |
| ① Значение блока = хвостовое выражение | Последний оператор присваивания → Void | ✅ Реализовано |
| ① Значение блока = хвостовое выражение | Пустой блок {} → Void | ✅ Реализовано |
| ① Защита обеспечивается проверкой типов | Согласование хвостового выражения с объявленным возвращаемым типом | ✅ Реализовано (#345) |
② return — нелокальный выход | Пробой через if / while / for / голый блок / вложенные блоки | ✅ Реализовано |
② Принцип взрыва Never <: T | Согласованность unify и is_subtype | ✅ Реализовано (исправлено внутреннее противоречие) |
③ if без else → Void | Значение ветви не утекает в позицию выражения | ✅ Реализовано (#346) |
| ① Значение блока = хвостовое выражение | Привязка значения голого блока x = { ... } | ✅ Реализовано (#343) |
| ① Значение блока = хвостовое выражение | Выход значения unsafe {} | ✅ Реализовано (#347) |
| ① Защита обеспечивается проверкой типов | Проверка побега для пустого блока / последнего оператора | ✅ Реализовано (открытый вопрос #342, п. 5) |
| ① Значение блока = хвостовое выражение | Выход значения spawn {} (хвостовое выражение) | ✅ Реализовано (#365) |
Покрытие корпусом: tests/yaoxiang/03-semantics/rfc010a_block_value.yx (позитивные) + tests/yaoxiang/06-compile-errors/tail_expr_type_mismatch{,_with_stmts}_err.yx (негативные).
Мотивация
Конфликт формулировок
Пример Фибоначчи на playground обнажил семантическое расхождение:
fib: (n: Int) -> Int = {
if n <= 1 {
return n // Этот return — от «if» или от «функции»?
}
return fib(n - 1) + fib(n - 2)
}Два принятых RFC дают противоположные выводы:
| RFC | Формулировка | Выведенная семантика |
|---|---|---|
| RFC-007 (принят) | На примере factorial заголовок гласит «Досрочный возврат: использование return»: if n <= 1 { return 1 } | return пробивает if и тело функции, возвращаясь в функцию |
| RFC-010 (принят) | «{} — это вычислительная единица, управляемая зависимостями… используйте return для явного возврата значения»; spawn { return c } возвращает результат задачи | return задаёт значение этому {} |
По RFC-010 фигурные скобки if n <= 1 { return 1 } — вычислительная единица, return 1 задаёт её значение как 1, значит значение if отбрасывается и следующая строка обязана выполниться — fib зацикливается. По RFC-007 — корректно.
Корневая причина: двойная роль слова return
- RFC-007 использует его для выражения «выход из функции» — концепция потока управления
- RFC-010 использует его для выражения «значение этого блока равно ему» — концепция вычисления
Использовать ключевое слово потока управления для выражения вычисления — категориальная ошибка (category error). Одно слово несёт две роли, и при любой настройке одна из сторон оказывается принесена в жертву.
Расширительная трактовка в нижестоящей документации
docs/src/reference/language-spec/syntax.md расширил «блок {}» из RFC-010 на все фигурные скобки:
- §2.9: «
returnвнутри{}всегда возвращает содержимое в вышестоящую область видимости» (с заявлением «унифицированная семантика: все блоки{}») - §3.3: «
returnиспользуется для возврата значения из блока кода»
Тогда как RFC-010 в оригинале определяет три блока со значением (= {} / spawn {} / unsafe {}), и нигде не затрагивает фигурные скобки if / while / for / match.
Текущее состояние реализации
| Поведение | Состояние |
|---|---|
if n == 0 { return 7 } … return 8 | Выход из функции (h(0) = 7) |
f = { n + 1 } | Хвостовое выражение доступно (возврат 5) |
x = { y = 5; y } (хвостовое выражение голого блока) | E3006 переменная не разрешена (см. #343) |
f: () -> Int = { if c {5} else {6} } | Возвращает void, хвостовое выражение отброшено (#344) |
f: () -> Int = { "s" } | Тихо проходит компиляцию (см. #345) |
x = if c { 19 } (без else) | 19 (должно быть Void, см. #346) |
v = unsafe { 42 } | void (см. #347) |
y = if c { 111 } else { 222 } | Доступно (if как выражение) |
tests/yaoxiang/03-semantics/no_tail_expr_return.yx ранее утверждал, что «хвостовое выражение больше неявно не возвращается», но этот случай не покрывался, поэтому тест не краснел. Этот файл заменён на tests/yaoxiang/03-semantics/tail_expr_and_return.yx.
Предложение
Три правила
① Значение блока = хвостовое выражение (единственный выход)
Значение оператора присваивания — Void; значение пустого блока {} — Void
② return : (T) -> Never
Нелокальный выход: выход из ближайшей границы функции
③ if без else → VoidЭтих трёх правил достаточно, чтобы вывести «досрочный возврат», без необходимости в дополнительном правиле, что return специфически относится к функции.
Правило ①: значение блока
Последний оператор/выражение блока является его значением (хвостовое выражение). Это не «нет хвостового выражения — значит Void» — непустой блок всегда имеет хвостовое выражение (сам последний оператор и является им), а пустой блок {} имеет значение Void.
// Хвостовое выражение определяет значение блока
a = {
x = compute() // Оператор присваивания → Void
x * 2 // Хвостовое выражение → значение блока
}
// Присваивание как хвостовое выражение → значение блока Void
b = {
x = compute()
log(x) // Оператор присваивания → Void
}
// Если нужно Void — пишите явно
c = {
log(x)
Void // Явный Void
}Проектный критерий (разделение механизма и защиты):
- Языковое правило задаёт только механизм: последний оператор есть значение блока; правило единственное, без неоднозначности
- Защита обеспечивается проверкой типов: объявление функции
-> Int, а тип хвостового выражения не совпадает → ошибка компиляции - Язык не защищает от «ошибки намерения»: если тип хвостового выражения случайно совпадает с возвращаемым типом, но по смыслу это не то, что задумано, — это недосмотр автора, язык не в состоянии это распознать. Не полагаться на языковые правила для защиты от ошибок намерения.
- Если возвращать не нужно — пишите
Voidявно
Это заменяет формулировку RFC-010 «= { ... } обязан использовать return, иначе вернётся Void» и её обоснование «необходим явный return для устранения неоднозначности, является ли последнее выражение возвращаемым значением».
Правило ②: return — нелокальный выход
Тип return — Never (нуль конструкторов, нет значений для заселения). Его семантика:
- Выход из ближайшей границы функции, передача значения вызывающему
- Пробой через все блоки — (при наличии)
if/while/for/match/ голый блок /spawn/unsafe
return не «возвращает в блок». { return n } как блок имеет значение n (правило хвостового выражения), тип Never; при этом return выполняет выход из функции. Оба факта выполняются одновременно.
Правило ③: if без else
x = if c { 19 } // без elseКогда условие ложно, вычислять нечего, берётся Void. Поэтому тип значения этого if — Void (или непригоден для не-Void позиции).
Когда значение нужно, явно указывайте обе ветви:
x = if c { 19 } else { 20 }Never — техническая основа сосуществования
Never <: T выполняется для любого типа T (принцип взрыва, см. спецификацию языка §Система типов). Следовательно:
fib: (n: Int) -> Int = {
if n <= 1 {
return n // Хвостовое выражение : Never
} // → значение этого if : Never
fib(n - 1) + fib(n - 2) // → значение блока : Int
}- Значение тела ветви
{ return n }—Never - Единственная ветвь этого
ifимеет типNever⇒ значениеif—Never - Оператор типа
Neverозначает прерывание последовательности здесь — следующий оператор не является «следующим в порядке выполнения» - А
Neverсводим к любому типу (принцип взрыва), поэтому весь блок удовлетворяет-> Int
«Досрочный возврат» естественно вытекает отсюда: прерывание последовательности Never + сводимость через принцип взрыва.
Детальное проектирование
Формализация блока и хвостового выражения
Block ::= '{' Stmt* '}' // Пустой блок → Void
| '{' Stmt* Expr '}' // Значение = Expr
Expr ::= ...
| Return // Тип Never
Stmt ::= Assignment | ExprStmt | ...
Значение(Block):
Пустой блок → Void
{ ...; e } → Тип(e)
{ ...; s } (s — оператор) → Void // Значение присваивания — VoidПравило типов для return
return e : Never где e : T
// Поскольку Never <: T' для любого T', return может находиться в любой позиции возвращаемого типаНикаких дополнительных правил для ограничения легитимности return не требуется — принцип взрыва уже покрывает это. Это снимает вопрос «может ли return находиться в функции, возвращающей X».
Слияние нескольких ветвей
join(A, B):
Если A : Never → B
Если B : Never → A
Иначе → требуется совместимость A и B (тот же тип или достижимая общая верхняя граница)Несколько ветвей if / match сливаются по join. Ветви с return не участвуют в слиянии, так как их тип — Never.
Согласованность с RFC-007
Примеры RFC-007 полностью соответствуют настоящему RFC и не требуют пересмотра:
factorial: (n: Int) -> Int = {
if n <= 1 { return 1 } // Ветвь Never, игнорируется join
return n * factorial(n - 1) // Выход из функции
}«Досрочный возврат» — не исключение, а следствие правил ①② и принципа взрыва.
Отличия от RFC-010 и требования к пересмотру
Определение RFC-010 пересмотрено согласно настоящему RFC, отличия следующие:
| Исходный пункт RFC-010 | Текущее определение |
|---|---|
«= { ... } обязан использовать return, иначе вернётся Void» | Значение блока = хвостовое выражение; пустой блок → Void |
Обоснование «явный return для устранения неоднозначности хвостового выражения» | Несостоятельно — хвостовое выражение не создаёт неоднозначности, return не мешает ему |
Три примера return c / return SqliteDb | Запись через хвостовое выражение (унифицированный выход значения spawn / unsafe) |
Основная конструкция RFC-010 ({} как вычислительная единица, управляемая зависимостями) сохраняется, только выход значения заменён с return на хвостовое выражение.
Единая перспектива (принцип проектирования)
Фигурные скобки if / while являются и императивным телом управления, и декларативной единицей вычисления — это различие перспектив, а не две языковые конструкции. Поэтому:
- Не проводить разделение на уровне языка между «телом управления» и «единицей вычисления»
- Все блоки используют единый набор правил вычисления (правило ①)
return— единственный механизм исключения, и его исключительность проистекает из теоретико-типовых свойствNever, а не из синтаксического особого случая
Это позволяет декларативному и питон-стилю сосуществовать естественно:
a = if input > 10 { 19 } else { 20 }
b = { x = f(); x * 2 }
c = spawn { fetch("a") }Компромиссы
Преимущества
- Устранение конфликта RFC: RFC-007 и RFC-010 из противоречия превращаются в отношение следствия, без жертв с какой-либо стороны
- Минимум правил: три правила + одно теоретико-типовое свойство (принцип взрыва), без исключений
- Единственное значение
return: выход из функции. Больше не нужно решать между «значение блока / значение функции» - Единая перспектива: императивный и декларативный стили сосуществуют, без разделения на уровне языка
- Согласованность со зрелыми языками: Rust также сочетает «хвостовое выражение +
return : !» для двух механизмов - Нулевой новый синтаксис: без новых ключевых слов, без изменений правил парсера
Недостатки
- Риск «случайного возврата» от последнего выражения: при забытом удалении последней строки возвращаемое значение молча меняется
- Смягчение: проверка типов перехватывает несоответствие типов; язык не обещает защиту от ошибок намерения (см. проектный критерий)
- Нижестоящая документация требует синхронного пересмотра: примеры в принятых документах должны быть переписаны
- Необходимо уточнить взаимодействие с завершением операторов: является ли последнее выражение, завершённое переносом строки, всё ещё значением блока (см. открытые вопросы)
Альтернативы
Вариант A: return сохраняет двойную роль, диспетчеризация по типу блока
В теле функции = {} return относится к функции; в spawn {} / unsafe {} — к блоку; в if {} / голом блоке — к чему?
Причина отклонения: принадлежность if неразрешима — это и есть исходный конфликт. И пользователю нужно помнить «каким блокам что принадлежит», без принципиального обоснования (почему if принадлежит функции, а голый блок — себе?).
Вариант B: Строгий возврат из блока (return всегда относится к текущему блоку)
Причина отклонения: утрата функциональности. return никогда не сможет выполнить досрочный выход из вложенного блока, guard clause (if err { return }) полностью невозможен, можно только писать функции как каскадно вложенные выражения.
Вариант C: Значение блока через новое ключевое слово (give x / yield x)
Причина отклонения: нарушает принцип нулевых синтаксических изменений RFC-036 (требуется новое ключевое слово), и пользователю придётся изучать две концепции (return для выхода + give для вычисления). Решение с хвостовым выражением не требует новых концепций.
Вариант D: Сохранить RFC-010 как есть (обязателен явный return)
Причина отклонения: непримиримый конфликт с RFC-007 (см. мотивацию). И реальная реализация уже пошла по пути хвостового выражения.
Пересмотр нижестоящей документации
Определение настоящего RFC стало авторитетной семантикой для нижестоящей документации; соответствующие документы синхронизированы (отменённые формулировки больше не сохраняются).
Открытые вопросы
- [x] Взаимодействие хвостового выражения с правилами завершения операторов из RFC-038: остаётся ли последнее выражение, завершённое переносом строки, значением блока? (@晨煦: требует согласования с правилом RFC-038 «начало строки
(/[никогда не объединяется» и др.) — эмпирически подтверждено: последние выражения, завершённые переносом строки (включая начало строки(/[/ литералы списков), являются значениями блока - [x] Когда
name = { ... }является функцией, а когда — привязкой значения блока — см. Приложение D (тип определяется содержимым) - [x] Конкретная форма
unsafe {}/spawn {}после пересмотра — оба хвостовых выражения эмпирически доступны - [x] Взаимодействие
joinветвейmatchс проверкой полноты (зависит от RFC-010b) — эмпирически подтверждено: ветвиNeverне участвуют в слиянии, поведениеjoinдляif/matchс несколькими ветвями корректно - [x] Диагностическое сообщение для пустого блока
{}как тела функции с не-Void возвращаемым типом — повторно используется существующийE1012, позиция указывает на аннотацию
Приложение A: Эмпирические доказательства
Все нижеследующее воспроизведено на 0.8.0.
| Код | Эмпирический результат |
|---|---|
h: (n)->Int = { if n==0 { return 7 } return 8 } | h(0)=7, h(1)=8 |
f: (n)->Int = { n + 1 } | 5 (хвостовое выражение доступно) |
f: ()->Int = { if c {5} else {6} } | void (должно быть 5, см. #344) |
f: ()->Int = if c {5} else {6} | 5 |
f: ()->Int = { match ... } | Корректно |
f: ()->Int = { while ...; i } | Корректно |
{ y = 5; y } как привязка выражения | E3006 (см. #343) |
f: ()->Int = { "s" } | Тихо проходит (см. #345) |
x = if c { 19 } (без else) | 19 (должно быть Void, см. #346) |
v = unsafe { 42 } | void (см. #347) |
y = if c { 111 } else { 222 } | 111 |
while { if i==2 { return 42 } } | 42 (пробой из цикла) |
Вложенный { { return 5 } return 1 } | 5 (пробой из голого блока) |
Приложение B: Записи проектных решений
| Решение | Решение | Обоснование | Дата |
|---|---|---|---|
Семантика return | Выход из функции, тип Never; не возвращает в блок | Устранение категориальной ошибки двойной роли | 2026-09-15 |
| Выход значения блока | Хвостовое выражение (единственный выход) | Минимум правил; согласовано с Rust | 2026-09-15 |
| Нет хвостового выражения | Не существует (непустой блок всегда имеет хвостовое выражение; пустой блок {} → Void) | Если нужно Void — пишите Void явно | 2026-09-15 |
| Значение присваивания | Void | Присваивание — оператор, а не производство значения | 2026-09-15 |
if без else | Void | При ложном условии вычислять нечего | 2026-09-15 |
| Разделение механизма и защиты | Язык задаёт механизм, проверка типов — защиту | Язык не обещает защиту от «ошибок намерения» | 2026-09-15 |
| Тело управления / единица вычисления | Не разделять на уровне языка, считать различием перспектив | Сосуществование императивного и декларативного стилей, избежание беспринципных исключений | 2026-09-15 |
| Обработка ошибочных примеров | Удалять напрямую, не оставлять ошибочный код | Сохранение кажущегося работоспособным ошибочного кода вводит в заблуждение | 2026-09-15 |
Приложение C: Глоссарий
| Термин | Определение |
|---|---|
| Хвостовое выражение | Последнее выражение, производящее значение, в блоке; значение блока равно ему |
| Нелокальный выход | Передача потока управления, проходящая через внешние единицы вычисления и действующая непосредственно на границу функции (return) |
| Принцип взрыва | Never <: T для любого T, что позволяет Never сводиться к любому типу |
| Блок со значением | = {} / spawn {} / unsafe {} — выход значения является хвостовым выражением |
| join | Правило слияния нескольких ветвей; ветвь Never не участвует в слиянии |
Приложение D: Разрешение неоднозначности функция / значение блока
Проблема
name = { ... } ранее определялся двумя принятыми RFC по-разному: RFC-007 (синтаксис функций) трактовал его как функцию («минимальный без аргументов» name = { return ... }), RFC-010 / 010a — как значение блока (= {} — блок со значением, значение — хвостовое выражение). Одна и та же синтаксическая позиция с двумя семантиками приводила к тому, что callable_parts() регистрировал блок как 0-аргументную функцию, а generate_block_ir брал значение блока — рассогласование двух слоёв понимания.
Разрешение: тип определяется содержимым
Один и тот же принцип должен пронизывать словарь и блок:
| Случай | Результат | Основание |
|---|---|---|
value является Lambda (=>) | Функция | => — явный конструктор функции |
Аннотация — Fn | Функция | Объявлен тип функции |
Аннотация — не Fn тип | Значение блока | Аннотация и есть тип (x: Int = {..}) |
| Без аннотации | Вывод по содержимому | Тип определяется содержимым |
x: Int = { y = 5; y } // Значение блока: x = 5 (аннотация не Fn)
f: () -> Int = { 5 } // Функция: f() = 5 (аннотация Fn)
f = { 5 } // Значение: f = 5 (нет аннотации → вывод по содержимому)
f = {} // Значение: f = Void (пустой блок)
b = () => 5 // Функция: явный lambda
d = { "a": 1 } // Значение: Dict (содержимое самоописывающее)Почему не «без аннотации — по умолчанию функция»: это привело бы к тому, что f = { 5 } — функция, а d = { "a": 1 } — словарь, и одна и та же { в одной и той же позиции без аннотации давала бы разные категориальные сущности. Все три рассмотренных основания несостоятельны:
- Нулевые изменения в существующем коде — цена миграции допустима (211 файл, 704 места), это не семантическое основание
- Определение функций — частое, значение блока — редкое — частота не является правилом типа
- RFC-007 принят, его изменение дороже изменения настоящего RFC — исправление неправильного не становится правильным из-за «дороговизны»
Тип определяется содержимым, а не наличием аннотации. Аннотация по-прежнему объявляет тип (f: () -> Int), но не навязывает значение по умолчанию из-за «отсутствия аннотации».
Размещение {}
Грамматика Dict требует минимум один ключ, у {} нет содержимого для опоры, поэтому берётся нулевая форма структуры блока → пустой блок, значение Void. Пустой словарь — через dict.new(). Подробности в спецификации §2.9.1.
Сопутствующие исправления реализации
- Аннотация не должна определять взятие значения: три места в
generate_function_irи др. использовалиreturn_type != Voidкак порог взятия значения хвостового выражения, что приводило к тихому отбрасыванию хвостового выражения дляf = { 5 }без аннотации и возвратуVoid. Аннотация лишь определяет, нужна ли проверка, а не нужно ли вычисление. callable_parts()больше не поглощает блок безусловно: диспетчеризация унифицирована черезExpr::block_binding_is_function(аннотация, значение).
Ссылки
- RFC-007: Унификация синтаксиса определения функций
- RFC-010: Унифицированный синтаксис типов
- RFC-038: Правила завершения операторов и переноса строк
- RFC-030: Механизм утверждений assert — применение уточнённых типов
Never - Спецификация языка §Система типов — позиционирование
Never/Voidкак ⊥ / ⊤ - Rust Reference:
!never type — изоморфное применение принципа взрыва
