RFC-010a: 末尾式の評価と return セマンティクス
参考:
- RFC-010: 統一型構文 —
name: type = valueモデル、{}依存駆動計算ユニット- RFC-007: 関数定義構文の統一方案 — コードブロックの返却ルール、早期 return
- RFC-038: 文の終端と改行ルール — 文と式の改行挙動
- 言語仕様 §型システム —
Neverのボトム原理
要約
return とブロック評価のセマンティクスを統一し、RFC-007 と RFC-010 の間の記述の衝突を解消する。
三つの自己無撞着なルールを提案する: ブロックの値は末尾式と等しい(唯一の出口);return は Never 型の非局所退出である(関数を脱出し、ブロックに「返す」のではない);if に else がない場合は Void を取る。
return とブロック評価は言語レベルで二分割しない——{ return n } をブロックとして見ればその値は n(型 Never)であり、同時に return の作用は関数を退出することである。この二つは ボトム原理(Never <: T)によって共存する。RFC-007 の「早期 return」と 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: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」が導出され、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を満たす
「早期 return」はここから自然に導出される: 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) // 関数を退出
}「早期 return」は特例ではなく、ルール ① ② とボトム原理の帰結である。
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() を使う。詳細は仕様 §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 — 同型のボトム原理の応用
