Skip to content

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つの問題がある:

  1. ボイラープレートが多い:各アサーションに4行必要で、テストファイルが肥大化する
  2. エラーメッセージが弱い:文字列を手動で連結するため、ソースコードの位置情報が欠落する
  3. 合成 불가능:アサーションを一括登録できず、テストフレームワークにパラメータとして渡せない

現在の問題

  • 統一されたアサーション機構が存在しない
  • テストコードに if + 印刷 + return のパターンが蔓延している
  • バイトコード層には既に Throw 命令があるが、言語層では公開されていない
  • RFC-011 でコンパイル時の Assert(C) 条件型が定義されているが、実行時の assert() は未実装である

設計原則

assert は YaoXiang 唯一のユーザー空間 panic 機構である。assert(false, "msg")raise と同等であり、別途 throw/raise キーワードは不要である。assert 関数は本身就是 if raise の最適な封裝である。

新しいキーワードを導入しない。新しい構文を導入しない。すべては関数呼び出しである。

案 A:ネイティブ関数

native 関数として assert を実装し、新しいキーワードは導入しない。

yaoxiang
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 值:

yaoxiang
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 を評価する
  2. true の場合、Unit を返す
  3. false の場合、実行時 panic をトリガー:
    • message の內容を出力する(もしあれば)
    • 呼び出しスタックを出力する(デバッグモードの場合)
    • 現在の発信を终止する

各オーバーロードの失敗動作

シグネチャ失敗時の動作
assert(false)デフォルト panic メッセージ
assert(false, "msg")文字列メッセージを出力後に panic
assert(false, error_val)Error 値をスロー
assert(Err(x))Err 内容を抽出して panic

コンパイル時 Assert との関係

assertAssert同一个精化原語の両面である——dispatch 分派パイプが「述語の自由変数がコンパイル時に到達可能か否か」に基づいて自動的に選択する:

条件分派動作
すべての自由変数がコンパイル時に既知CompileTime → 証明パイプProved → 擦除、Disproved → コンパイルエラー、Unknown → 証明要求
実行時の自由変数が存在Runtime → チェック挿入Bool チェック + 流敏感仮定セット Γ に精化事実を注入
yaoxiang
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/ 以下にネイティブ関数を登録するだけでよい:

  1. src/std/assert.rs を新規追加
  2. std.assert.assertstd.assert.Assert を登録(後者はコンパイル時条件型、#155 参照)
  3. 内部的には既存の BytecodeInstr::Throw 命令を呼び出す

利点

  • 構文変更ゼロ:純粋な関数で、新しいキーワードが不要
  • 新しい概念ゼロ:既存のネイティブ関数登録メカニズムを再利用
  • 高い拡張性:関数シグネチャは assert_eq などの变体にも拡張可能(将来)
  • 自己文書化std.assert 名前空間本身就是ドキュメントである

欠点

  • コンパイル時に知れない:案 B(キーワード)と異なり、コンパイル時の死コード除去が行えない統一案では已经不成立。CompileTime モードの assert は証明パイプをパスし、コンパイル時に既知の cond は擦除またはコンパイルエラーとなる(assert(false) → Never → 死コード)。
  • デバッグモードでのみ呼び出しスタックを取得可能

案 B:組込みキーワード(統一案に取代済み)

弃却済み。案 A と案 B の対立は dispatch 分派パイプによって解消された——assert は Assert の値導入子であり、コンパイル時に既知の場合は証明パイプをパスし(実行時オーバーヘッドゼロ)、実行時はチェックをパスする。「関数」と「キーワード」の間で二者選択する必要がない。以下は歴史的記録である。

yaoxiang
assert(1 + 1 == 2, "math is broken")

型シグネチャ

独立した型シグネチャはない——キーワードは parser が処理する。

実行時動作

案 A と同じ。

コンパイラの改动

parser、AST、型チェック、IR 生成の変更が必要:

  1. parser:新しい Expr::Assert バリアントを追加
  2. AST:新しい Expr::Assert ノードを追加
  3. 型チェック:パラメータの型を検証
  4. 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] assertmessage なしの簡略形 assert(cond) をサポートする必要があるか?サポートする。assert(cond, ?msg)、message は任意である。
  • [x] assert_eqassert_ne などの变体が必要か?不要。YAGNI。テストフレームワークが形になってからの话说。
  • [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 の「完全に独立」結論に取代)

結論assertAssert は2つの独立したメカニズムではない。assert: (Bool) -> Assert(IsTrue(cond)) ——dispatch が自動的に分派を行う:

  • コンパイル時に既知 → 証明パイプに進む(Proved は擦除 / Disproved はエラー / Unknown は証明が必要)
  • 実行時入力 → チェックを挿入 + Γ 仮定を注入

モジュール構造std.assert は実行時アサーション(assert)とコンパイル時精化型(AssertIsTrue)を统一的にサポートする。「分开实现」ではなく、同一原語の両面である。

2026-07-11:assert オーバーロード設計

問題assert はなぜ2つのオーバーロードが必要で、統一的な (Bool, ?String) ではだめなのか?

解答

実行時の assert() は YaoXiang 唯一のユーザー空間 panic 機構である。assert(false, "msg") は他の言語の raise/throw と同等である。因此,它需要覆盖三种场景:

  1. 条件 + 简单消息:assert(cond, "msg")
  2. 条件 + 自定义 Error:assert(cond, my_error)
  3. Result 检查:assert(result) — 最も簡潔な if is_err { panic }

Result オーバーロードの合理性は、これがエラー伝播の最短パスであるためである——「Result は Ok であるべき、そうでなければ死」。先に .is_ok() してから個別にエラーを処理する必要がない。

付録 B:設計意思決定記録

意思決定決定日付記録者
案 A と案 B の選択統一案:dispatch 分派パイプが A/B の対立を解消、assert は Assert の値導入子2026-07-12chenxu
message は任意かはいassert(cond, ?msg)、String または Error2026-07-11chenxu
assert_eq などの变体が必要か不要。YAGNI、テストフレームワークを待つ2026-07-11chenxu
別途 raise/throw キーワードが必要か不要assert(false, msg) は raise と同等2026-07-11chenxu
assert と Assert の関係一体两面assert: (Bool) -> Assert(IsTrue(cond))、dispatch が自動的に分派2026-07-12chenxu

参考文献