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

  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:ネイティブ関数 ​

assert をネイティブ関数として実装し、新しいキーワードを導入しない。

yaoxiang
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 値を取れる:

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. 第 1 引数 condition: Bool を評価する
  2. true の場合、Unit を返す
  3. 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 チェック + フロー敏感仮定集合 Γ に精緻化事実を注入
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")  # 运行时 check

2026-07-12 統一方案:それまでの「完全独立」結論は置き換えられた。assert() は Assert の値導入子であり、dispatch により自動振り分けされる。

コンパイラの修正 ​

parser、AST、typecheck、IR gen の修正は不要。

src/std/ 下にネイティブ関数の登録を追加するだけでよい:

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

利点 ​

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

欠点 ​

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

案 B:組み込みキーワード(統一方案により置き換え済み) ​

廃止済み。案 A と案 B の対立は dispatch 振り分けパイプラインにより解消される。assert は Assert の値導入子であり、コンパイル時既知なら証明パイプライン(実行時オーバーヘッドゼロ)を、実行時なら check を取る。「関数」と「キーワード」の二者択一は不要。以下は歴史的記録。

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

型シグネチャ ​

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

実行時の振る舞い ​

案 A と同じ。

コンパイラの修正 ​

parser、AST、typecheck、IR gen の修正が必要:

  1. parser:Expr::Assert バリアントを追加
  2. AST:Expr::Assert ノードを追加
  3. typecheck:引数の型を検証
  4. 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] assert_eq、assert_ne などのバリアントは必要か? → 不要。YAGNI。テストフレームワークが形になってから検討。
  • [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 つのシナリオをカバーする必要がある:

  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-12晨煦
message が任意かどうかはい:assert(cond, ?msg)、String または Error2026-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晨煦

参考文献 ​