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 曾声称「尾表达式不再隐式返回」,但未覆盖该 case,故测试未红。该文件已被 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        where 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 的类型论性质,不是语法特例

这使声明式与 Python 式写法自然共存:

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 : !」共存两机制
  • 零新语法:不加关键字,不改 parser 语法规则

缺点 ​

  • 末位表达式即值有「误返回」风险:末行忘删时返回值静默改变
    • 缓解:类型检查拦截类型不符;语言不承诺防意图错(见设计判据)
  • 下游文档需同步修订:已接受文档的示例要改写
  • 语句终止的交互需明确:换行终止的末位表达式是否仍为块值(见开放问题)

替代方案 ​

方案 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] match 分支的 join 与穷尽性检查的交互(依赖 RFC-010b)——已实测:Never 分支不参与合并, 多分支 if / match 的 join 行为正确
  • [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 就显式写 Void2026-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()。详见 spec §2.9.1。

伴随的实现修正 ​

  1. 注解不得决定取值:generate_function_ir 等三处曾用 return_type != Void 当尾表达式的取值门槛,导致无注解的 f = { 5 } 尾表达式被静默丢弃、返回 Void。注解只决定要不要检查,不决定要不要求值。
  2. callable_parts() 不再无条件吞块:分流改由 Expr::block_binding_is_function(注解, 值) 统一判定。

参考文献 ​