---
title: 'RFC-030: assert アサーション機構'
status: '承認済み'
author: '晨煦'
created: '2026-06-15'
updated: '2026-07-14'
decision:
'assert と Assert は表裏一体であり、dispatch により自動振り分けされる。6 Phase
すべて実装済み(#157〜#162 はクローズ済み)。std.assert モジュールに統一登録(#169
はクローズ済み)、assert ネイティブ関数と Assert/IsTrue 型族は同じ経路。'
issue: '#97'
issues_impl:
- '#155'
- '#157'
- '#158'
- '#159'
- '#160'
- '#161'
- '#162'
- '#169'
---
# RFC-030: assert アサーション機構
## 概要
YaoXiang に `assert`
アサーション機構を導入する。テスト、前置条件のチェック、実行時 panic に使用する。`assert`
とコンパイル時精緻化型 `Assert(C)`(RFC-011
§4.3 参照)は**同じ精緻化プリミティブの二面**であり、dispatch により「述語の自由変数がコンパイル時に到達可能か」に応じて、コンパイル時証明または実行時チェックに自動振り分けされる。`assert(false, "msg")`
は `raise` と同等であり、独立した `throw`/`raise` キーワードは不要である。
## 動機
### なぜこの機能が必要か?
現在の YaoXiang の E2E テストは、`if` + `io.println` + `return`
でアサーションを模倣することしかできない:
```yaoxiang
val = some_func()
if val != 42 {
io.println("FAIL: expected 42")
return
}
```この記述方法には 3 つの問題がある:
- ボイラープレートが多い:各アサーションに 4 行必要で、テストファイルが肥大化する
- エラーメッセージが貧弱:文字列を手動で連結しており、ソースコードの位置情報がない
- 合成できない:アサーションを一括登録できず、テストフレームワークに引数として渡せない
現状の問題
- 統一されたアサーション機構が存在しない
- テストコードに
if+ 出力 +returnのパターンが充斥している - バイトコード層には既に
Throw命令があるが、言語層では公開されていない - RFC-011 でコンパイル時
Assert(C)条件型が定義されているが、実行時assert()は未実装
設計原則
assert は YaoXiang 唯一のユーザーランド panic 機構である。assert(false, "msg") は raise と同等であり、独立した throw/raise キーワードは不要。assert 関数自体が if raise の最良のカプセル化である。
新しいキーワードは導入せず、新しい構文も導入しない。すべては関数呼び出しである。
案 A:ネイティブ関数
assert をネイティブ関数として実装し、新しいキーワードを導入しない。
use std.assert.assert
main: () -> Void = {
assert(1 + 1 == 2, "math is broken")
assert(get_name() == "YaoXiang", "name mismatch")
}オーバーロードシグネチャ
assert には 2 つのオーバーロードがある:
// 核心シグネチャ:assert は Assert の値宇宙における導入子
assert: (cond: Bool, ?msg: String | Error) -> Assert(IsTrue(cond))
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 戻り値は精緻化型であり、() ではない
//
// IsTrue: Bool -> Type は真偽値から型への橋渡し:
// IsTrue(true) = Void (⊤、プログラム続行)
// IsTrue(false) = Never (⊥、発散/コンパイルエラー)assert の実際の振る舞いは dispatch の振り分けによって決定される:
- すべての自由変数がコンパイル時既知 → CompileTime:コンパイラが cond を評価し、true → Void として消去、false → コンパイルエラー(Never は居住不能)
- 実行時自由変数が存在 → Runtime:check を挿入し、フロー敏感仮定集合 Γ に精緻化事実を注入する
オプションのメッセージ ?msg および Result オーバーロード(以下参照)は、実行時 raise ペイロードとして保持される。
オーバーロード 1:条件アサーション (Bool, ?String | Error)
Bool + 任意のメッセージ。メッセージは String または Error 値を取れる:
assert(1 + 1 == 2) // メッセージなし、デフォルトの panic メッセージ
assert(1 + 1 == 2, "math is broken") // 文字列メッセージ
assert(x > 0, my_error) // Error 値を直接渡すassert(false, "msg") は YaoXiang の raise/throw 等価体であり、独立したキーワードは不要。
オーバーロード 2:Result アサーション (Result)
単一の Result 引数を取り、自動的に Err かどうかをチェックする:
利点
- 構文変更ゼロ:純粋関数で、新しいキーワードは不要
- 新概念ゼロ:既存のネイティブ関数登録機構を再利用
- 高い拡張性:関数オーバーロードにより複数のシグネチャを自然にサポート
- 自己文書化:
std.assert名前空間自体がドキュメント
欠点
- なし。assert の型シグネチャが正しければ、コンパイラは関数到達性解析によってデッドコードを推論できる。追加の pass は不要。
実行時の振る舞い
- 第 1 引数
condition: Boolを評価する trueの場合、Unitを返すfalseの場合、実行時 panic を発火する:messageの内容を出力する(ある場合)- コールスタックを出力する(デバッグモード時)
- 現在の実行を終了する
各オーバーロードの失敗時の振る舞い
| シグネチャ | 失敗時の振る舞い |
|---|---|
assert(false) | デフォルトの panic メッセージ |
assert(false, "msg") | 文字列メッセージを出力後 panic |
assert(false, error_val) | Error 値を throw する |
assert(Err(x)) | Err の内容を取り出して panic |
コンパイル時 Assert との関係
assert と Assert は同じ精緻化プリミティブの二面であり、dispatch 振り分けパイプラインにより「述語の自由変数がコンパイル時に到達可能か」に応じて自動選択される:
| 条件 | 振り分け | 振る舞い |
|---|---|---|
| すべての自由変数がコンパイル時既知 | CompileTime → 証明パイプライン | Proved → 消去、Disproved → コンパイルエラー、Unknown → 証明要求 |
| 実行時自由変数が存在 | Runtime → check 挿入 | Bool チェック + フロー敏感仮定集合 Γ に精緻化事実を注入 |
use std.assert
# 编译期已知(泛型参数)—— 走 CompileTime,零运行时开销
Array: (T: Type, N: Int) -> Type = {
data: Array(T, N),
length: assert.Assert(N > 0), # N 是泛型参数,编译期求值
}
# 运行时值 —— 走 Runtime,插入 Bool 检查
x = read_int()
assert.assert(x > 0, "expected positive") # 运行时 check2026-07-12 統一方案:それまでの「完全独立」結論は置き換えられた。
assert()はAssertの値導入子であり、dispatch により自動振り分けされる。
コンパイラの修正
parser、AST、typecheck、IR gen の修正は不要。
src/std/ 下にネイティブ関数の登録を追加するだけでよい:
src/std/assert.rsを新規追加std.assert.assertとstd.assert.Assert(後者はコンパイル時条件型)を登録- 内部で既存の
BytecodeInstr::Throw命令を呼び出す
利点
- 構文変更ゼロ:純粋関数で、新しいキーワードは不要
- 新概念ゼロ:既存のネイティブ関数登録機構を再利用
- 高い拡張性:関数シグネチャは
assert_eqなどのバリアントにも拡張可能(将来) - 自己文書化:
std.assert名前空間自体がドキュメント
欠点
コンパイル時に知ることができない:案 B(キーワード)と異なり、コンパイル時にデッドコード除去を行えない→ 統一方案の下では成立しない。CompileTime モードの assert は証明パイプラインを通り、コンパイル時既知の cond → 消去またはコンパイルエラー(assert(false)→ Never → デッドコード)。- デバッグモードでのみコールスタックを取得可能
案 B:組み込みキーワード(統一方案により置き換え済み)
廃止済み。案 A と案 B の対立は dispatch 振り分けパイプラインにより解消される。assert は Assert の値導入子であり、コンパイル時既知なら証明パイプライン(実行時オーバーヘッドゼロ)を、実行時なら check を取る。「関数」と「キーワード」の二者択一は不要。以下は歴史的記録。
assert(1 + 1 == 2, "math is broken")型シグネチャ
独立した型シグネチャはない。キーワードは parser が処理する。
実行時の振る舞い
案 A と同じ。
コンパイラの修正
parser、AST、typecheck、IR gen の修正が必要:
- parser:
Expr::Assertバリアントを追加 - AST:
Expr::Assertノードを追加 - typecheck:引数の型を検証
- IR gen:
BytecodeInstr::Throwを生成
利点
- コンパイル時にソースコードの位置が分かる(debug info に依存しない)
- コンパイル時の定数畳み込みが可能:
assert(true)→ 空操作、assert(false)→ コンパイルエラー
欠点
| 欠点 | 影響 |
|---|---|
| パーサの修正が必要 | 新しい構文ノードを導入し、メンテナンスコストが増大する |
| キーワードは拡張不可 | assert_eq などのバリアントも依然として関数として必要 |
| コンパイル時の利点は非実用的 | 以下の分析を参照 |
比較
| ディメンション | 案 A(関数) | 案 B(キーワード) |
|---|---|---|
| 実装コスト | 約 20 行 | parser + AST + typecheck + IR gen |
| 構文変更 | なし | 新しいキーワード |
| 拡張性 | 関数オーバーロード | 配套のマクロが必要 |
| ソースコード位置 | debug info | コンパイル時に取得可能 |
| 定数畳み込み | pass のサポートが必要 | コンパイル時に取得可能 |
| 実行時オーバーヘッド | 関数呼び出し | 極小 |
コンパイル時分析の現実的制約
案 B の核心的な利点であるコンパイル時分析は、定数畳み込み pass がなければ有効化されない。すなわち、コンパイラは assert(false) 中の false をコンパイル時に評価して初めて、これがデッドコードであることを知る。
YaoXiang には現在定数畳み込み pass がない。仮に案 B を採用しても、assert(x > 0) のような一般的な記述はコンパイル時には分析できない。assert(true) / assert(false) のようなリテラルのみが分析可能である。
したがって、案 B のコンパイル時の利点は現段階では理論的であり、実用的ではない。
未解決問題
- [x]
案 A と案 B のどちらかを選択?→ 統一方案:assert は Assert の値導入子。案 A と案 B の対立は dispatch 振り分けパイプラインにより解消される。コンパイル時既知なら証明パイプラインを、実行時なら check を取る。「二者択一」は不要。 - [x]
→ サポートする。assertはmessageを伴わない簡略形assert(cond)をサポートすべきか?assert(cond, ?msg)、message は任意。 - [x]
→ 不要。YAGNI。テストフレームワークが形になってから検討。assert_eq、assert_neなどのバリアントは必要か? - [x]
panic の出力にソースコードの位置は含まれるか?→ 案 A は debug info(コールスタック)に依存する。 - [x]
assert / Assert 統一問題→ 確定。統一方案:assert: (Bool) -> Assert(IsTrue(cond))、表裏一体、dispatch による自動振り分け。Never型(⊥)をassert(false)の戻り型として組み込む。
2026-07-05:案 A を選択(統一方案により置き換え済み)
案 A の 20 行実装が価値とコストの双方で勝出した。2026-07-12 に統一方案が確定した後、案 A/B の対立は dispatch 振り分けパイプラインにより解消された。assert は Assert の値導入子であり、もはや「関数」と「キーワード」の二者択一は存在しない。
2026-07-12:統一方案確定(2026-07-11 の「完全独立」結論を置き換え)
結論:assert と Assert は 2 つの独立した機構ではない。assert: (Bool) -> Assert(IsTrue(cond)) —— dispatch により自動振り分けされる:
- コンパイル時既知 → 証明パイプラインへ進む(Proved 消去 / Disproved エラー / Unknown 証明要求)
- 実行時入力 → check を挿入 + Γ 仮定を注入
モジュール構造:std.assert が実行時アサーション(assert)とコンパイル時精緻化型(Assert、IsTrue)を統一的に担う。「分けて実装」することはなく、同じプリミティブの二面である。
2026-07-11:assert オーバーロード設計
問題:なぜ assert には 2 つのオーバーロードが必要で、統一された (Bool, ?String) ではいけないのか?
解答:
実行時 assert() は YaoXiang 唯一のユーザーランド panic 機構である。assert(false, "msg") は他言語の raise/throw と等価である。したがって、以下の 3 つのシナリオをカバーする必要がある:
- 条件 + 単純なメッセージ:
assert(cond, "msg") - 条件 + カスタム Error:
assert(cond, my_error) - Result チェック:
assert(result)— 最も簡潔なif is_err { panic }
Result オーバーロードの合理性は、これがエラー伝播の最短経路であるという点にある。「Result は Ok であるべき、さもなくば死」。先に .is_ok() を呼び、エラーを別途処理する必要はない。
付録 B:設計決定記録
| 決定事項 | 決定 | 日付 | 記録者 |
|---|---|---|---|
| 案 A と案 B の選択 | 統一方案:dispatch 振り分けパイプラインが A/B の対立を解消、assert は Assert の値導入子 | 2026-07-12 | 晨煦 |
| message が任意かどうか | はい:assert(cond, ?msg)、String または Error | 2026-07-11 | 晨煦 |
| assert_eq などのバリアントが必要か | 不要。YAGNI、テストフレームワークができてから検討 | 2026-07-11 | 晨煦 |
| 独立した raise/throw キーワードが必要か | 不要。assert(false, msg) が raise と同等 | 2026-07-11 | 晨煦 |
| assert と Assert の関係 | 表裏一体。assert: (Bool) -> Assert(IsTrue(cond))、dispatch による自動振り分け | 2026-07-12 | 晨煦 |
参考文献
- RFC-007: 関数定義構文統一方案 —
name: type = valueモデル - RFC-010: 型構文統一方案 — 型システムの基礎
- RFC-011: ジェネリクスシステム設計 §4.3 — コンパイル時検証と
Assert(C)条件型 - RFC-026: FFI 中核機構 — ネイティブ関数登録機構
- RFC-027: コンパイル時述語と統一静的検証 — コンパイル時評価システム
