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 曾声称「尾表达式不再隐式返回」,但未覆盖该 case,故测试未红。该文件已被 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 // 赋值的值是 Voidreturn 的类型规则
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,无需修订:
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 式写法自然共存:
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 就显式写 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()。详见 spec §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 — 同构的爆炸原理应用
