RFC-009: 所有権モデルの設計
要約
本文書は YaoXiang プログラミング言語の**所有権モデル(Ownership Model)**を定義する。
核心設計——5 つの概念、1 つのグラデーション:
ちら見/その場変更 奪う 共有保持 複製する システムレベル
│ │ │ │ │
&T Move ref clone() unsafe
&mut T ゼロコピー コンパイラ自動 明示的ディープコピー *T
ゼロサイズトークン デフォルト Rc/Arc 選択 ユーザ責任
型属性自然
権限を推論- Move(デフォルト):代入/引数渡し/戻り値 = 所有権移転、ゼロコピー、RAII による自動解放
&T/&mut T(借用トークン):ゼロサイズのコンパイル時トークン型。&Tは複製可能(共有読み取り)、&mut Tは線形(排他的書き込み)。権限は型属性から自然に推論され、特別なルールは不要。戻り値や構造体フィールドへの格納も可能。refキーワード:スコープを跨いだ共有。コンパイラが Rc(タスクを跨がない)か Arc(タスクを跨ぐ)を自動選択clone():明示的なディープコピーunsafe+*T:生ポインタ、システムレベルのエスケープハッチ
除去された複雑性:
- ❌ ライフタイム
'aなし - ❌ 独立した借用検査フレームワークなし(借用衝突はホーア命題に次元削減され、型検査と証明パイプラインを共有)
- ❌ GC なし
- ❌「エスケープ禁止」のような特別ルールなし(トークンは通常の型であり、スコープは型システムで統一的に処理)
- ❌ ユーザは Rc/Arc の違いを知る必要がない(コンパイラが自動選択)
プログラミング負担:
&Tは複製可能、&mut Tは複製不可——2 つの型属性、0 つの特別ルール、コンパイラは完全自動。 性能保証:Move はゼロオーバーヘッド、トークンはゼロオーバーヘッド(ゼロサイズ型、コンパイル後に消滅)、ref は必要に応じたコスト、GC による一時停止なし。
動機
なぜ所有権モデルが必要か?
| 言語 | メモリ管理 | 問題 |
|---|---|---|
| C/C++ | 手動管理 | メモリリーク、ダングリングポインタ、二重解放 |
| Java/Python | GC | レイテンシ変動、メモリオーバーヘッド、予測不能な停止 |
| Rust | 所有権 + 借用検査 | ライフタイム 'a の学習曲線が急峻 |
| YaoXiang | Move + Token + ref | シンプルで確定的な GC なし |
設計目標
# 1. デフォルトは Move(ゼロコピー)
p = Point(1.0, 2.0)
p2 = p # Move、p は以降読めない
# 2. &T / &mut T 借用トークン(ゼロオーバーヘッド、型属性から権限を自然に推論)
print_info(p2) # コンパイラが &Point トークンを自動生成し、使用後解放
shift(p2, 1.0, 1.0) # コンパイラが &mut Point トークンを自動生成
# 3. ref = 共有(コンパイラが Rc/Arc を自動選択)
shared = ref p2 # スコープを跨いで保持
spawn { use(shared) } # コンパイラ:タスクを跨ぐ → Arc
# 4. clone() = 明示的コピー
backup = p2.clone() # ディープコピー、独立
# 5. unsafe + *T = システムレベル
unsafe {
ptr: *Point = &p
(*ptr).x = 0.0
}Rust との核心的な違い
| 特性 | Rust | YaoXiang |
|---|---|---|
| デフォルトのセマンティクス | 借用 &T(明示的に .clone() が必要) | Move(値渡し、ゼロコピー) |
| 借用 | &T/&mut T、返却可能、ライフタイムが必要 | &T/&mut T ゼロサイズトークン、Dup/Linear 型属性から自然に権限を推論 |
| 共有メカニズム | Arc::new() + 手動 Weak | ref キーワード(コンパイラが Rc/Arc を自動選択) |
| コピー | clone() | clone() |
| 生ポインタ | *T | *T |
| ライフタイム | 'a | ❌ なし |
| 借用検査 | グローバルな推論 | 型検査器が借用命題を自動生成し、証明パイプラインで統一検証 |
| 循環参照 | 手動 Weak | タスク終了時に統一解放 / タスク跨ぎは lint / 標準ライブラリ Weak |
提案
1. Move(デフォルトの所有権移転)
# ルール:代入 / 引数渡し / 戻り値 = Move、ゼロコピー
p: Point = Point(1.0, 2.0)
p2 = p # Move、p は以降読めない
# 変数は再代入可能(Python 風、シャドウイングなし)
p = Point(3.0, 4.0) # p を再バインド、型は一致必須
# 関数引数:Move
process: (p: Point) -> Point = {
p.transform()
p # Move で返却
}
# 関数の戻り値:Move
create: () -> Point = {
p = Point(1.0, 2.0)
p # Move で返却、ゼロコピー
}特徴:
- ゼロコピー(コンパイラがポインタを移動)
- 移動後、元のバインドは読めない(コンパイルエラー)
- RAII:スコープ終了時に自動解放
- 関数シグネチャ
(T) -> T自体がドキュメント——T を消費し、T を返す
2. &T / &mut T(借用トークン)
核心原則:&T と &mut T はゼロサイズのコンパイル時トークン型である。これらは「参照」ではなく、「アクセス権限の型レベル証明」である。
2.1 2 つの型属性
&T → ゼロサイズ、ソースデータを凍結(ReadToken 生存中は WriteToken を禁止)、
凍結保証のもとで複数の読み取り専用ビューが安全 → 複製可能(Dup)
&mut T → ゼロサイズ、排他的読み書き(WriteToken 生存中は他のあらゆるトークンを禁止)、
排他的アクセス下では複製は無意味 → 線形(non-Dup)因果関係を逆転させてはならない:凍結が原因、Dup は結果。 &T が Dup を実装しているから共存できる——ということではない。データが凍結されている(変異可能性がない)からこそ、複数の読み取り専用ビューが安全であり、Dup が実現可能なのである。Dup を定義とし、衝突検査を「追加のパッチ」と捉えると、設計を誤る。
2.2 基本使用方法
# メソッド側:引数型を宣言し、必要な権限を決定
Point.print: (self: &Point) -> Void = {
print(self.x) # &Point トークンが読み取り権限を付与
print(self.y)
}
Point.shift: (self: &mut Point, dx: Float, dy: Float) -> Void = {
self.x = self.x + dx # &mut Point トークンが書き込み権限を付与
self.y = self.y + dy
}
# 呼び出し側:コンパイラが借用と Move を自動選択
p = Point(1.0, 2.0)
p.print() # コンパイラが &Point トークンを自動生成
p.shift(1.0, 1.0) # コンパイラが &mut Point トークンを自動生成
p.print() # OK、shift 呼び出し終了で前のトークンは解放済み
# 自由関数も同様
distance: (a: &Point, b: &Point) -> Float = {
sqrt((a.x - b.x)**2 + (a.y - b.y)**2) # 2 つの &Point トークンが共存——Dup 型
}
d = distance(p, p2)2.3 なぜ「エスケープ禁止」が不要か
RFC-009 v8 では &T/&mut T に対して 3 つの特別ルールを課していた——引数にのみ使用可、返却不可、構造体に格納不可。これは「借用」概念にパッチを当てていた。
トークンシステムにはこれらのルールは不要。トークンは通常の型であり、他のすべての型と同じスコープルールに従う。
参照の返却——自然にサポート:
# ✅ トークンは戻り値とともに伝播
Point.get_x: (self: &Point) -> (&Float, &Point) = {
return (&self.x, self) # 子トークンと親トークンを一緒に返却
}
# 使用
p = Point(1.0, 2.0)
(px_ref, p) = p.get_x() # トークンが呼び出し元に返却される
print(px_ref) # OK、トークンはまだスコープ内構造体への格納——自然にサポート:
# ✅ 構造体はトークンをフィールドとして持てる
Window: Type = {
target: Point,
view: &Point, # トークンフィールド——target への読み取り専用ビューを保持
}
# view のトークンは target から派生し、Window は両方の所有権を保持
# Window が存在する限り、view トークンは有効2.3 クロージャと Lambda の明示的な引数
Lambda は関数値——返却、格納、現在のスコープ外への持ち出しが可能。したがって Lambdaは外側のローカル変数を暗黙的にキャプチャしない。外側のデータが必要な場合は、明示的な引数として渡す。
# ✅ Lambda は明示的な引数を使用
double: (x: Int) -> Int = (x) => x * 2
filter_by: (items: List(Int), f: (Int) -> Bool) -> List(Int) = { ... }
# ✅ spawn { } はこのルールの影響を受けない——spawn は即座に実行される並列ブロックで、親タスクは完了を待機
shared = ref data
spawn { use(shared) }
# ❌ Lambda は外側の変数を暗黙的にキャプチャできない
x = 42
f = () => { x + 1 } # コンパイルエラー:x はスコープ外
# ✅ 正しい方法:明示的に引数で渡す
f = (x) => { x + 1 }
f(x)
# ✅ 正しい方法その 2:コンテキストを作成時点で固定(カリー化)——クロージャは引数のみを取り、キャプチャしない
gt: (t: Int) -> (x: Int) -> Bool = (x) => x > t
evens = list.filter(nums, gt(threshold))補足(2026-08-17):コンテキスト依存の正解はカリー化による固定であり、キャプチャではない。クロージャがエスケープした後、その定義箇所のスコープは既に死んでいる可能性があるため、暗黙的にキャプチャしてはならない。しかし呼び出し時点(作成時点)のスコープは必ず生存しており、コンテキストはその時点で値としてクロージャ内に固定されるのが安全である。SPEC §12.3 を参照。
spawn { } は関数値ではない。 spawn でマークされたブロックは if/while 本体と同様に——即座に実行され、親スタックフレームが生存中に完了する。spawn 本体は外側の変数に通常通りアクセス可能。
タスクを跨ぐ——トークンはスレッドを跨げない:
# ❌ トークンはタスク境界を跨げない
bad_task: (p: &Point) -> Void = {
spawn { print(p.x) } # ❌ コンパイルエラー:トークンはタスクを跨いで渡せない
}
# これは特別ルールではない——トークンはコンパイル時の権限証明であり、タスクを跨いだ共有は ref を使用
# タスクを跨いだ共有が必要な場合は、ref を使用トークンは ref できない:
# ❌ トークンは権限証明であり、所有権ではない
bad_ref: (p: &Point) -> Void = {
shared = ref p # ❌ コンパイルエラー:&T は所有可能な型ではない
}2.4 トークンのライフタイム
トークンのライフタイムは通常のスコープルールによって決定され、ライフタイムパラメータは不要。
- 関数引数内のトークン:呼び出し中に生存し、呼び出し終了後に解放
- 返却されたトークン:所有権を呼び出し元に移転
- 構造体に格納されたトークン:構造体とともに生存
コンパイラは 'a アノテーションを必要としない。なぜならトークンは値であり、値のライフタイムは所有権システム(Move/RAII)によって統一的に管理されるからだ。借用問題を所有権問題に次元削減する。
2.5 トークン衝突検出
トークン衝突検出はホーア論理命題であり、独立したフロー依存解析ではない。
{衝突する ReadToken がすべて死亡} write(data) {WriteToken は安全に取得可能}型検査やユーザ述語検証と RFC-027 の証明パイプラインを共有する。コンパイラが借用命題(borrow_conflict、use_after_move、use_after_drop、mut_violation)を自動生成し、パイプラインに投入して検証する。パイプラインは Proved / Disproved / Unproven を返す。
# ❌ &mut トークンは線形であり、複製不可
bad_dup: (p: &mut Point) -> Void = {
p2: &mut Point = p # Move、p は以降読めない
p.x = 10.0 # ❌ コンパイルエラー:WriteToken は既に移動済み
}
# ✅ &T トークンは Dup 型であり、自由に複製可能
good_dup: (p: &Point) -> Void = {
p2: &Point = p # OK、&T は Dup 型
print(p.x) # OK
print(p2.x) # OK、2 つの読み取り専用トークンが共存
}借用検査は消えていない——次元削減された。 既存の BorrowChecker は BorrowPredicateEmitter(命題生成器)となり、生成された借用命題は他の型命題と同じ証明パイプラインを共有する。これは型検査器の概念と完全に並行する:型検査器は型等価命題を生成し、借用命題生成器は借用命題を生成し、同一のパイプラインが検証する。詳細は RFC-009a を参照。
2.7 コンパイラ内部:ブランド機構
ユーザがブランドに触れることは決してない。コンパイラは内部で各トークンにコンパイル時一意識別子を割り当てる。
ユーザに見えるもの コンパイラ内部表現
────────────────────────────────────────
&Point → ReadToken(Point, #N) // #N はコンパイル時一意な整数
&mut Point → WriteToken(Point, #M) // #M はコンパイル時一意な整数ブランドの用途:
- 偽造防止:トークンは所有者のカプセルからのみ取得でき、凭空には構築できない
- 関連追跡:
&Pointから&Float(フィールドアクセス)を派生する際、&Floatは派生ブランド(#N.field_x)を運び、コンパイラは親トークンまで追跡可能 - 衝突検出:同源の
WriteTokenと派生ReadTokenは同時にアクティブになれない
ブランドは単態化とインライン化後に完全に消滅し、生成される機械語には存在しない。ランタイムオーバーヘッドはゼロ。
2.8 自動借用選択ルール
呼び出し側コンパイラは以下の優先順位で自動選択する:
1. 実引数が後に使用される場合 → トークン生成を優先(&T または &mut T、メソッドシグネチャに従う)
2. 実引数が後に使用されない場合 → Move
3. 優先マッチング順序:&T < &mut T < Move# 例:自動選択
p = Point(1.0, 2.0)
p.print() # print は &self を宣言 → コンパイラが &Point トークンを生成
p.shift(1.0, 1.0) # shift は &mut self を宣言 → コンパイラが &mut Point トークンを生成
p2 = p # Move、p は以降使用されない2.9 RFC-009 v8 簡素版借用との比較
| 特性 | 簡素版借用 (v8) | 借用トークン (v9) |
|---|---|---|
| 参照の返却 | ❌ ハードコードで禁止 | ✅ トークンは戻り値とともに伝播 |
| 構造体への格納 | ❌ ハードコードで禁止 | ✅ トークンは構造体フィールドとして保持可能 |
| Lambda の明示的引数 | ❌ ハードコードで禁止 | ✅ Lambda は明示的引数を使用 |
| 特別ルール | 3 つ(引数のみ/返却不可/格納不可) | 0 つ——型属性から自然に推論 |
| 借用検査 | 専用の交差借用検査 | 型検査器のフロー依存活性分析 |
| ライフタイムアノテーション | 不要 | 不必要 |
| ランタイムオーバーヘッド | ゼロ | ゼロ(ゼロサイズ型、コンパイル後消滅) |
| エラーメッセージ | 「借用はエスケープできない」 | 「WriteToken(#3) は既に移動済み」(通常の型エラー) |
| ユーザのメンタルモデル | 「借用」の特別な位置づけを理解する | &T は複製可能、&mut T は複製不可 |
3. ref キーワード(コンパイラの自動最適化)
ref はスコープを跨いだ共有の唯一の方法。内部実装が Rc か Arc かは、ユーザが気にする必要はない。
3.1 基本使用方法
p: Point = Point(1.0, 2.0)
shared = ref p # 共有、コンパイラが実装を自動選択
# タスクを跨いだ共有
@block
main: () -> Void = {
data = ref heavy_data
spawn { use(data) } # コンパイラ:タスクを跨ぐ → Arc
spawn { use(data) } # コンパイラ:タスクを跨ぐ → Arc
}
# 単一タスク内での共有
@block
main: () -> Void = {
data = ref heavy_data
use(data) # コンパイラ:タスクを跨がない → Rc
}ユーザのメンタルモデル:ref = 共有保持。それだけで十分。
3.2 コンパイラのエスケープ解析:Rc vs Arc
ref のデータフロー解析:
他タスクへエスケープしない → Rc(非アトミック参照カウント、低オーバーヘッド)
他タスクへエスケープする → Arc(アトミック参照カウント、スレッドセーフ)3.3 循環検出戦略
タスク内循環 → 静かに許可。
├── 各タスクには明確なライフタイム境界がある——タスク終了時にすべてのリソース(ref 循環を含む)が統一解放される。
├── 長期実行サービスはリクエスト/接続ごとに子タスクを作成すべき——子タスク終了で自動回収され、累積リークしない。
├── ref は常に生存し続け、セマンティクスは混じり気なし。
└── ユーザはタスク内で双方向強参照を構築する権限を持つ(例:グラフ計算の中間状態)。
タスク跨ぎ循環 → lint(デフォルトは warn、設定可能)。
├── プログラムの挙動は正しく、実際にリークはしない(親タスク終了時に子タスクのリソースが全解放される)。
├── しかしタスク跨ぎ強参照は所有権境界が曖昧であることを意味し、再考する価値がある。
├── デフォルトは warn レベル、コンパイルは成功するがヒントが表示される。
└── チーム設定で deny にでき、CI 品質ゲートに組み込める。Lint レベル(Rust の clippy に類似):
| レベル | 挙動 | シナリオ |
|---|---|---|
allow | チェックしない | 個人プロジェクト |
warn(デフォルト) | コンパイル成功、ヒント表示 | 開発段階 |
deny | コンパイル失敗 | チーム CI 品質ゲート |
forbid | コンパイル失敗、上書き不可 | 組織レベルの強制ルール |
# タスク内循環:静かに許可、双方向強参照
build_graph: () -> Void = {
a = Node("a")
b = Node("b")
a.next = ref b
b.prev = ref a # 循環。タスク終了時に統一解放。
}
# タスク跨ぎ循環:lint(デフォルト warn)
@block
parent_task: () -> Void = {
shared_a = ref a
shared_b = ref b
spawn {
shared_a.child = ref shared_b # ⚠️ warn: タスク跨ぎ循環参照
}
}プロジェクト設定例:
# yaoxiang.toml
[lints]
cross-task-cycle = "deny" # タスク跨ぎ循環は CI で即座に拒否| 循環タイプ | 挙動 | 理由 |
|---|---|---|
| タスク内 ref 循環 | チェックしない | ユーザの権限、タスク終了時に統一解放 |
| タスク跨ぎ ref 循環 | lint(デフォルト warn) | 再考を促す、deny に設定可能 |
3.4 Weak:標準ライブラリで提供
use std.weak
# 上級ユーザの明示的選択
a.next = ref b
b.prev = std.weak.new(a.next) # ユーザがどの方向を弱くするかを明示的に制御Weak は言語組み込みではなく、標準ライブラリの型である。 日常的には ref で十分。メモリを細かく制御したい上級ユーザは手動で Weak を導入する。
2026-08-03 改訂:独立した
std.weakモジュールとして実装(std.rcは存在しない——refは言語キーワードでありモジュールではない;モジュールパスはstd.weakに統一され、構築/昇格エントリはstd.weak.new/std.weak.upgrade)。初稿で想定していたstd.rc.Weakは採用されず、本改訂が優先される。
3.5 借用トークン vs ref
&T / &mut T | ref | |
|---|---|---|
| 何をする | ちら見/その場変更 | 共有保持 |
| 範囲 | トークン値のスコープに従う | スコープを跨ぐ |
| コスト | ゼロオーバーヘッド(ゼロサイズ型) | Rc または Arc(コンパイラが選択) |
| エスケープ | 可(トークンは戻り値/構造体/クロージャで伝播) | 本来エスケープ用途 |
| タスク跨ぎ | 不可(トークンはコンパイル時権限証明であり、タスクを跨いで渡せない) | 可(コンパイラが Arc を自動選択) |
| 循環 | 関与しない | タスク内は静かに許可、タスク跨ぎは lint |
4. clone() —— 明示的コピー
p: Point = Point(1.0, 2.0)
p2 = p.clone() # ディープコピー
# p と p2 は独立しており、相互に影響しないいつ使うか:元の値を保持しつつ、Move にも共有にも適さないシナリオ。
5. unsafe + 生ポインタ(システムレベルプログラミング)
p: Point = Point(1.0, 2.0)
unsafe {
ptr: *Point = &p # 生ポインタ
(*ptr).x = 0.0 # デリファレンス(ユーザが安全性を保証)
ptr2 = ptr + 1 # ポインタ演算
}制限:
unsafeブロック内でのみ使用可能- ユーザはダングリング、解放後使用がないことを保証
- FFI、メモリ操作などのシステムレベルプログラミングに使用
6. 所有権グラデーション概要
借用トークン(ゼロオーバーヘッド) Move(ゼロオーバーヘッド) 共有(必要に応じたコスト) コピー
│ │ │ │
&T 複製可能トークン デフォルトの所有権移転 ref Rc/Arc clone()
&mut T 線形トークン チェイン消費バックフロー コンパイラが自動選択 明示的ディープコピー
│ │ │ │
トークン値のスコープ内 スコープ内 スコープを跨ぐ いつでも
返却可/構造体に格納可 T -> T バックフロー ref タスク跨ぎ → Arc 独立コピー
ゼロサイズ、コンパイル後消滅 T -> Void 消費 ref タスク跨ぎなし → Rc
ゼロサイズ、コンパイル後消滅 タスク内循環は静かに許可
タスク跨ぎ循環は lint
標準ライブラリ Weak でエスケープ総合例
Point: Type = {
x: Float,
y: Float,
# &T:読み取り専用トークン
print: (self: &Point) -> Void = {
print(self.x)
print(self.y)
}
# &mut T:可変トークン
shift: (self: &mut Point, dx: Float, dy: Float) -> Void = {
self.x = self.x + dx
self.y = self.y + dy
}
# Move → Move:消費バックフロー
scale: (self: Point, f: Float) -> Point = {
self.x = self.x * f
self.y = self.y * f
self # 奪い、変更し、返す
}
# 参照を返す:トークンは戻り値で伝播
get_x: (self: &Point) -> (&Float, &Point) = {
return (&self.x, self)
}
}
# Lambda の明示的引数
double: (x: Int) -> Int = (x) => x * 2
# 総合使用
p = Point(1.0, 2.0)
p.print() # &Point トークン
p.shift(1.0, 1.0) # &mut Point トークン
p = p.scale(2.0) # Move → バックフロー
shared = ref p # ref 共有
spawn { use(shared) }
# 独立したクローン
backup = p.clone()
# タスク内循環:静かに許可
a = Node("a")
b = Node("b")
a.next = ref b
b.prev = ref a # 循環、タスク終了時に統一解放
# unsafe システムレベル
unsafe {
ptr: *Point = &p
(*ptr).x = 0.0
}型システム制約
Dup 型属性
Dup(Duplicable)はコンパイラが自動管理する型属性であり、シャローコピーを意味する:代入/引数渡し時にはハンドル/トークンがコピーされ、基底データが共有される。これは Move(所有権移転)や Clone(明示的ディープコピー、独立コピー作成)と 3 段階グラデーションを成す。
Dup と Clone は直交する概念——Dup はハンドルをコピーしてデータを共有し、Clone は独立したコピーを作成する。1 つの型が Dup と Clone の両方をサポートすることも、いずれか一方のみをサポートすることも可能。
| 型 | Dup | Clone | 説明 |
|---|---|---|---|
&T | ✅(トークン複製、複数ビューが同じデータを指す) | ✅ | 読み取り専用トークン |
ref T | ✅(参照カウント+1、ヒープデータを共有) | ✅ | 共有保持(コンパイラが Rc/Arc を自動選択) |
| String, Bytes | ✅(内部参照カウント、ハンドル共有で基底バッファを共有) | ✅ | 文字列/バイト |
&mut T | ❌(線形、排他的) | ❌ | 可変トークン |
*T | ❌ | ❌ | 生ポインタ |
| struct | 派生(全フィールドが Dup の場合に自動派生) | ✅ | 構造体 |
プリミティブ値型(Int, Float, Bool, Char)の代入挙動はコンパイラ組み込みの値コピーである——2 つの値は完全に独立しており、シャローコピーではない。これらは Dup 型属性には属さず、コンパイラのネイティブ処理である。
性能分析
| 操作 | コスト | 説明 |
|---|---|---|
| Move | ゼロ | ポインタ移動 |
&T / &mut T | ゼロ | ゼロサイズ型、コンパイル後消滅、ランタイムオーバーヘッドゼロ |
ref(タスク跨ぎなし) | 低 | Rc にコンパイル、非アトミック操作 |
ref(タスク跨ぎ) | 中 | Arc にコンパイル、アトミック操作 |
clone() | 型による | 小オブジェクトは高速、大オブジェクトは低速 |
unsafe + *T | ゼロ | 直接メモリ操作 |
比較
| 言語 | 共有メカニズム | メモリ管理 | 循環処理 | 複雑度 |
|---|---|---|---|---|
| Rust | Arc / Mutex + 借用検査 | コンパイル時検査 | 手動 Weak | 高 |
| Go | chan / pointer | GC | GC | 低 |
| C++ | shared_ptr | RAII | weak_ptr | 中 |
| YaoXiang | ref + 借用トークン | RAII | タスク境界解放 / タスク跨ぎ lint / 標準ライブラリ Weak | 低 |
トレードオフ
利点
- 統一:
&T/&mut Tは通常の型であり、特別な言語機能ではない。RFC-010 のname: type = valueと完全に一貫 - シンプル:ライフタイムなし、借用検査は型システム命題に次元削減。
&Tは複製可能、&mut Tは複製不可——2 つの型属性 - 強力:参照返却、構造体格納、クロージャキャプチャが可能——表現力は Rust と同等
- コンパイラのスマート化:ref が Rc/Arc を自動選択、呼び出し側が借用を自動選択
- 決定論的:ref は確実に生存させ続け、密かに弱参照には変わらない
- 高性能:Move はゼロコピー、トークンはゼロオーバーヘッド(ゼロサイズ型、コンパイル後消滅)
- 柔軟:
unsafe + *Tでシステムレベルプログラミングをサポート
欠点
- ジェネリックブランドパラメータの伝染:トークンがブランド識別子を運ぶため、参照を返す関数のシグネチャに追加のジェネリックパラメータが現れる
- ref のランタイムオーバーヘッド:アトミック操作にコストがある(ただしこれは共有の本質的代償)
- unsafe のリスク:ユーザが正当性を保証しなければならない
- タスク跨ぎ循環は lint でありコンパイルエラーではない:Rust のようにコンパイルエラーとならず、デフォルトが warn であるため、deny をチーム設定して初めて品質ゲートとなる
代替案
| 案 | 採用しない理由 |
|---|---|
| GC | ランタイムオーバーヘッドがあり、停止を予測できない |
| Rust 借用検査器 | ライフタイム 'a が必要、学習曲線が急峻 |
| 純粋な Move | 並列共有を処理できない |
| 生ポインタなし | システムレベルプログラミングができない |
| Rc/Arc をユーザに公開 | 実装詳細をユーザに押し付け、認知負荷が増大する |
| 簡素版借用(v8) | エスケープ禁止戦略がクロージャキャプチャや参照返却などの重要な表現力を犠牲にする |
設計決定記録
| 決定 | 決定 | 理由 | 日付 |
|---|---|---|---|
| デフォルト値 | Move(ゼロコピー) | 高性能、ゼロオーバーヘッド | 2025-01-15 |
| 共有メカニズム | ref キーワード、コンパイラの自動最適化 | ユーザはシンプル、コンパイラが担当 | 2025-01-15 |
| 借用 | &T/&mut T をゼロサイズトークン型に | 型属性(Dup/Linear)から自然に権限を推論、型システムを統一 | 2025-01-15 |
| 借用トークン | 簡素版借用を置き換え、&T Dup、&mut T Linear | 「エスケープ禁止」などの特別ルールを除去、クロージャキャプチャ/参照返却/構造体格納をサポート | 2026-05-29 |
| コピー | clone() | 明示的なセマンティクス | 2025-01-15 |
| システムレベル | *T + unsafe | システムプログラミングをサポート | 2025-01-15 |
| ライフタイム | 実装しない | トークンは値であり、ライフタイムは Move/RAII で統一管理、借用を所有権問題に次元削減 | 2025-01-15 |
| Rc/Arc | コンパイラが自動選択し、ユーザには不可視 | 認知負荷を低減 | 2025-01-15 |
| 循環参照 | タスク内はチェックなし、タスク跨ぎは lint(デフォルト warn) | 構造化並行の自然な保証、lint は deny に設定可能 | 2025-01-16 |
| Weak | 標準ライブラリで提供 | 上級ユーザの明示的選択 | 2025-01-16 |
| 消費分析 | 削除 | ミニ借用検査器は不要 | 2026-05-11 |
| 所有権バックフロー | 削除 | (T) -> T シグネチャ自体がドキュメント | 2026-05-11 |
| 空状態の再利用 | 削除(機能として) | Move 後の再代入は自然な挙動 | 2026-05-11 |
| 逆関数/部分消費/フィールド三層可変性 | 削除 | 過剰設計 | 2026-05-11 |
| Lambda の非暗黙的キャプチャ | Lambda は明示的引数のみを使用し、外側変数を暗黙的にキャプチャしない;コンテキストはカリー化により作成時点で固定(SPEC §12.3) | クロージャ定義箇所のスコープは既に死んでいる可能性がある;作成時点(呼び出しスコープ生存中)での値固定は安全 | 2026-06-16 |
バージョン履歴
| バージョン | 主要変更 | 日付 |
|---|---|---|
| v1 | 初稿:Rust 所有権モデルに基づく | 2025-01-08 |
| v8 | 過剰設計を削除(逆関数/部分消費/フィールド三層可変性/消費分析/所有権バックフロー/空状態再利用)、簡素版借用 &T/&mut T を追加 | 2026-05-11 |
| v9 | 借用トークンシステムが簡素版借用を置き換え、型システムを統一;トークン衝突検出をホーア命題に修正、RFC-009a 参照 | 2026-06-13 |
未決議議題
| 議題 | 説明 | 状態 |
|---|---|---|
| Drop 構文 | 明示的な drop() 関数の必要性 | 議論待ち |
| エスケープ解析アルゴリズム | ref のタスク跨ぎ検出実装 | 議論待ち |
| トークン衝突検出 | ホーア論理命題、後述参照 | ✅ 解決済み(詳細は RFC-009a 参照) |
トークン衝突検出:ホーア論理命題
トークン衝突検出の完全な解決策は RFC-009a: トークンライフタイム解析——ホーア証明パイプラインに基づく を参照。核心ポイント:
トークン活性はホーア論理命題である。{衝突する ReadToken がすべて死亡} write(data) {WriteToken は安全に取得可能}——型検査やユーザ述語検証と RFC-027 の証明パイプラインを共有する。コンパイラが借用命題(borrow_conflict、use_after_move、use_after_drop、mut_violation)を自動生成し、パイプラインが Proved / Disproved / Unproven を返す。
借用検査は消えていない——次元削減された。 BorrowChecker は BorrowPredicateEmitter となり、検査ではなく命題を生成する。これは「型検査器」の概念と完全に並行する:型検査器は型等価命題を生成し、借用命題生成器は借用命題を生成し、同一のパイプラインが検証する。
ブランド ID(#42)は 'a である。 情報は完全に同一で、エンコーディングが異なる。'a は型シグネチャで見えるが、#42 はコンパイラ内部にある。新しい解析を発明したのではなく、ライフタイムを型層から証明層に降ろしたのである。
アルゴリズム概要(詳細は RFC-009a 参照):
- ブランドツリーのプレフィックスマッチング → 衝突トークンの特定(O(depth)、深さ ≤ 3)
- 後向き BFS → コンシューマから出発、break で逆辺を切断、構造解析で 95%+ のシナリオをカバー(高速パス)
- SMT ロジック切断 → while + パス条件時のみ呼び出し(低速パス、極めて稀)
参考文献
YaoXiang 公式ドキュメント
外部参考
ライフサイクルと帰属
| 状態 | 場所 | 説明 |
|---|---|---|
| ドラフト | docs/design/rfc/ | 著者の草稿、提出審査待ち |
| 審査中 | docs/design/rfc/ | オープンなコミュニティ議論とフィードバック |
| 受理済み | docs/design/accepted/ | 正式な設計ドキュメントになる |
| 却下済み | docs/design/rfc/ | RFC ディレクトリに保持 |
