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 でアサーションを模擬する必要がある:
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:ネイティブ関数
native 関数として assert を実装し、新しいキーワードは導入しない。
use std.assert.assert
main = {
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))
// ^^^^^^^^^^^^^^^^^^^^^^^^
// Unit を返すのではなく、精化型を返す
//
// IsTrue: Bool -> Type は真値から型への橋渡し:
// IsTrue(true) = Void (⊤、プログラム続行)
// IsTrue(false) = Never (⊥、発散/コンパイルエラー)assert の実際の動作は dispatch 分派で决定される:
- すべての自由変数がコンパイル時に既知の場合 → CompileTime:コンパイラが cond を評価し、true → Void に擦除、false → コンパイルエラー(Never は居住不可)
- 実行時の自由変数が存在する場合 → Runtime:チェックを挿入し、流敏感仮定セット Γ に精化事実を注入
任意のメッセージ ?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 は不要。
実行時動作
- 最初のパラメータ
condition: Boolを評価する trueの場合、Unitを返すfalseの場合、実行時 panic をトリガー:messageの內容を出力する(もしあれば)- 呼び出しスタックを出力する(デバッグモードの場合)
- 現在の発信を终止する
各オーバーロードの失敗動作
| シグネチャ | 失敗時の動作 |
|---|---|
assert(false) | デフォルト panic メッセージ |
assert(false, "msg") | 文字列メッセージを出力後に panic |
assert(false, error_val) | Error 値をスロー |
assert(Err(x)) | Err 内容を抽出して panic |
コンパイル時 Assert との関係
assert と Assert は同一个精化原語の両面である——dispatch 分派パイプが「述語の自由変数がコンパイル時に到達可能か否か」に基づいて自動的に選択する:
| 条件 | 分派 | 動作 |
|---|---|---|
| すべての自由変数がコンパイル時に既知 | CompileTime → 証明パイプ | Proved → 擦除、Disproved → コンパイルエラー、Unknown → 証明要求 |
| 実行時の自由変数が存在 | Runtime → チェック挿入 | 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") # 実行時チェック2026-07-12 統一案: 이전의「完全に独立」結論は取り消された。
assert()はAssertの値導入子であり、dispatch が自動的に分派を行う。
コンパイラの改动
parser、AST、型チェック、IR 生成の変更は不要である。
src/std/ 以下にネイティブ関数を登録するだけでよい:
src/std/assert.rsを新規追加std.assert.assertとstd.assert.Assertを登録(後者はコンパイル時条件型、#155 参照)- 内部的には既存の
BytecodeInstr::Throw命令を呼び出す
利点
- 構文変更ゼロ:純粋な関数で、新しいキーワードが不要
- 新しい概念ゼロ:既存のネイティブ関数登録メカニズムを再利用
- 高い拡張性:関数シグネチャは
assert_eqなどの变体にも拡張可能(将来) - 自己文書化:
std.assert名前空間本身就是ドキュメントである
欠点
コンパイル時に知れない:案 B(キーワード)と異なり、コンパイル時の死コード除去が行えない→ 統一案では已经不成立。CompileTime モードの assert は証明パイプをパスし、コンパイル時に既知の cond は擦除またはコンパイルエラーとなる(assert(false)→ Never → 死コード)。- デバッグモードでのみ呼び出しスタックを取得可能
案 B:組込みキーワード(統一案に取代済み)
弃却済み。案 A と案 B の対立は dispatch 分派パイプによって解消された——assert は Assert の値導入子であり、コンパイル時に既知の場合は証明パイプをパスし(実行時オーバーヘッドゼロ)、実行時はチェックをパスする。「関数」と「キーワード」の間で二者選択する必要がない。以下は歴史的記録である。
assert(1 + 1 == 2, "math is broken")型シグネチャ
独立した型シグネチャはない——キーワードは parser が処理する。
実行時動作
案 A と同じ。
コンパイラの改动
parser、AST、型チェック、IR 生成の変更が必要:
- parser:新しい
Expr::Assertバリアントを追加 - AST:新しい
Expr::Assertノードを追加 - 型チェック:パラメータの型を検証
- IR 生成:
BytecodeInstr::Throwを生成
利点
- コンパイル時にソースコードの位置情報を持てる(デバッグ情報に依存しない)
- コンパイル時に定数畳み込みが可能:
assert(true)→ 空操作、assert(false)→ コンパイルエラー
欠点
| 欠点 | 影響 |
|---|---|
| parser の改动が必要 | 新しい構文ノードを導入し、保守コストが増大 |
| キーワードは拡張不可 | assert_eq などの变体は依然として関数が必要 |
| コンパイル時の優位性は実用的でない | 下記分析参照 |
比較
| 次元 | 案 A(関数) | 案 B(キーワード) |
|---|---|---|
| 実装コスト | ~20 行 | parser + AST + 型チェック + IR 生成 |
| 構文変更 | なし | 新しいキーワード |
| 拡張性 | 関数のオーバーロード | 配套マクロが必要 |
| ソースコード位置 | デバッグ情報 | コンパイル時に取得可能 |
| 定数畳み込み | 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 分派パイプによって解消された——コンパイル時に既知の場合は証明パイプをパスし、実行時はチェックをパスする。「二者選択」は不要。 - [x]
→ サポートする。assertはmessageなしの簡略形assert(cond)をサポートする必要があるか?assert(cond, ?msg)、message は任意である。 - [x]
→ 不要。YAGNI。テストフレームワークが形になってからの话说。assert_eq、assert_neなどの变体が必要か? - [x]
panic 出力にソースコードの位置情報を含めるか?→ 案 A はデバッグ情報に依存(呼び出しスタック)。 - [x]
assert / Assert 統一問題→ 既に確定。統一案:assert: (Bool) -> Assert(IsTrue(cond))、一体两面、dispatch が自動的に分派。詳細については #156(クローズ済み)を参照。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 は証明が必要)
- 実行時入力 → チェックを挿入 + Γ 仮定を注入
モジュール構造:std.assert は実行時アサーション(assert)とコンパイル時精化型(Assert、IsTrue)を统一的にサポートする。「分开实现」ではなく、同一原語の両面である。
2026-07-11:assert オーバーロード設計
問題:assert はなぜ2つのオーバーロードが必要で、統一的な (Bool, ?String) ではだめなのか?
解答:
実行時の assert() は YaoXiang 唯一のユーザー空間 panic 機構である。assert(false, "msg") は他の言語の raise/throw と同等である。因此,它需要覆盖三种场景:
- 条件 + 简单消息:
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 | chenxu |
| message は任意か | はい:assert(cond, ?msg)、String または Error | 2026-07-11 | chenxu |
| assert_eq などの变体が必要か | 不要。YAGNI、テストフレームワークを待つ | 2026-07-11 | chenxu |
| 別途 raise/throw キーワードが必要か | 不要。assert(false, msg) は raise と同等 | 2026-07-11 | chenxu |
| assert と Assert の関係 | 一体两面。assert: (Bool) -> Assert(IsTrue(cond))、dispatch が自動的に分派 | 2026-07-12 | chenxu |
参考文献
- RFC-007: 関数定義構文統一案 —
name: type = valueモデル - RFC-010: 統一型構文 — 型システム基礎
- RFC-011: ジェネリックシステム設計 §4.3 —コンパイル時検証と
Assert(C)条件型 - RFC-026: FFI 中核メカニズム — ネイティブ関数登録メカニズム
- RFC-027: コンパイル時述語と統一静的検証 — コンパイル時評価システム
