Skip to content

RFC-010a: Вычисление хвостового выражения и семантика return ​

Ссылки:

Резюме ​

Унифицировать семантику 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 обнажил семантическое расхождение:

yaoxiang
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.

yaoxiang
// Хвостовое выражение определяет значение блока
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 ​

yaoxiang
x = if c { 19 }        // без else

Когда условие ложно, вычислять нечего, берётся Void. Поэтому тип значения этого if — Void (или непригоден для не-Void позиции).

Когда значение нужно, явно указывайте обе ветви:

yaoxiang
x = if c { 19 } else { 20 }

Never — техническая основа сосуществования ​

Never <: T выполняется для любого типа T (принцип взрыва, см. спецификацию языка §Система типов). Следовательно:

yaoxiang
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 и не требуют пересмотра:

yaoxiang
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, а не из синтаксического особого случая

Это позволяет декларативному и питон-стилю сосуществовать естественно:

yaoxiang
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
Выход значения блокаХвостовое выражение (единственный выход)Минимум правил; согласовано с Rust2026-09-15
Нет хвостового выраженияНе существует (непустой блок всегда имеет хвостовое выражение; пустой блок {} → Void)Если нужно Void — пишите Void явно2026-09-15
Значение присваиванияVoidПрисваивание — оператор, а не производство значения2026-09-15
if без elseVoidПри ложном условии вычислять нечего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 = {..})
Без аннотацииВывод по содержимомуТип определяется содержимым
yaoxiang
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 } — словарь, и одна и та же { в одной и той же позиции без аннотации давала бы разные категориальные сущности. Все три рассмотренных основания несостоятельны:

  1. Нулевые изменения в существующем коде — цена миграции допустима (211 файл, 704 места), это не семантическое основание
  2. Определение функций — частое, значение блока — редкое — частота не является правилом типа
  3. RFC-007 принят, его изменение дороже изменения настоящего RFC — исправление неправильного не становится правильным из-за «дороговизны»

Тип определяется содержимым, а не наличием аннотации. Аннотация по-прежнему объявляет тип (f: () -> Int), но не навязывает значение по умолчанию из-за «отсутствия аннотации».

Размещение {} ​

Грамматика Dict требует минимум один ключ, у {} нет содержимого для опоры, поэтому берётся нулевая форма структуры блока → пустой блок, значение Void. Пустой словарь — через dict.new(). Подробности в спецификации §2.9.1.

Сопутствующие исправления реализации ​

  1. Аннотация не должна определять взятие значения: три места в generate_function_ir и др. использовали return_type != Void как порог взятия значения хвостового выражения, что приводило к тихому отбрасыванию хвостового выражения для f = { 5 } без аннотации и возврату Void. Аннотация лишь определяет, нужна ли проверка, а не нужно ли вычисление.
  2. callable_parts() больше не поглощает блок безусловно: диспетчеризация унифицирована через Expr::block_binding_is_function(аннотация, значение).

Ссылки ​