Ante: 借用検査と参照カウントを組み合わせる新しい方式
(verdagon.dev)- Anteは参照カウントの柔軟性と借用検査の安全性を併用しつつ、Rust式のランタイムパニックやSwift式の排他的アクセス検査のオーバーヘッドを避けようとするシステム言語設計である
- 中核となる仕組みはshape-stabilityとtemporary uniq conversionで、参照カウントされた値のフィールドには安全に可変借用を作り、ユニオン内部の値は限定されたスコープでのみ
uniqとして扱う - Rustの
Rc<RefCell<T>>は使い方を誤るとランタイムでパニックを起こしうる一方、Swiftのborrowing systemはランタイムの排他的アクセス検査を含むが、Anteは一部のケースをコンパイル時の規則で処理しようとしている - まだ一部しか実装されていないwork-in-progressの設計であり、型を再帰的に解析して特定のオブジェクトへ到達可能かどうかを判断する必要があるため、フィールド追加がbreaking API changeになりうる
- このアプローチは、shared mutable borrowingは常に不可能だという前提を弱め、Vale、group borrowing、Rust GhostCellのような手法とともに、メモリ安全性設計における例外領域を広げている
Anteが目指す組み合わせ
- Anteはメモリ安全性とスレッド安全性を備えた、より単純なRustを目指すシステムプログラミング言語である
- 基本モデルは単一所有権と借用検査で、値はスタックや、それを含む構造体・配列の中にインラインで配置される
- 単純さを優先したいときは、型に
sharedキーワードを付けて参照カウントを選べる shared type Color、shared type RbTree tを使ったred-black treeのbalance関数は、Pythonの例と同じくらい短く、C++やRustの例よりも小さい- 中心的な関心は、参照カウントされたデータを可変で借用するときに、Rustの
borrow_mut()のパニックリスクやSwiftのランタイム排他的アクセス検査なしでどう扱うかである - Anteはまだwork-in-progressの段階で、一部は実装済み、一部は理論段階で、設計も変化中である
shape-stabilityと複数の可変参照
- Anteのshape-stabilityは、「stable shapeを持つ対象への参照は、ほかでどんな変更が起きても常に有効である」という概念である
- この概念により、同じ構造体に対して複数の可変借用参照を同時に持てる
heal (healer: mut Entity) (target: mut Entity)の例では、同じEntityを2つの引数に渡して、自分自身を回復するself_heal呼び出しが可能であるhealerとtargetが同じEntityを指していても、このコードではEntityを破壊できないため、2つの参照は有効なままである
- 構造体そのものとそのフィールド、さらにフィールドのフィールドへの可変参照も同時に許可されうる
ship: mut Spaceshipとengine_alias: mut Engine = ship.engineを同時に使っても、関数実行中にshipとその中のengineが破壊されないと判断する
- RustやSwiftでは、同じデータを複数の
&mut参照が同時に指す形は許可されない
参照カウントされた値のフィールド可変借用
- Anteでは型定義の前に
sharedを付けると、その型は自動的に参照カウントされる shared mut type Spaceshipの例では、launchがRcに相当するSpaceshipを保持しつつ、mut ship.engineをset_fuelに渡すlaunchが包含オブジェクトであるSpaceshipを保持しているため、そのフィールドであるengineも生存していると判断できる- 一般規則として、
shared mut型のフィールドに対しては常にmut借用参照を作れる- ただし、そのフィールドの内部にあるすべての対象に対して常に可変借用を作れるわけではなく、別の規則が必要になる
- 以後の例では、糖衣構文である
shared mut type Spaceshipの代わりに、より明示的なRc Spaceship表記を使うshared mut type Spaceshipはtype Spaceshipとなり、var ship: Spaceshipはvar ship: Rc Spaceshipとなる
ユニオンが安全性の問題を生む箇所
- ユニオンは内容をインラインで保持するため、ポインタ追跡やキャッシュミスを減らせ、速度の面で有利である
- Cの
union Engineがstruct Spaceshipの中に入ると、StringTheoryEngineとImpulseEngineはSpaceshipのメモリ内に配置される - Javaのようにインターフェースとポインタを使う方式と対比される
- Cの
- 問題は、メモリ安全言語でユニオンを安全にサポートするのが難しい点にある
EngineがStringTheoryEngine(str: String)またはImpulseEngine(fuel: I32)である例では、shipとother_shipが同じSpaceshipを指しているとセグメンテーションフォルトが起こりうるmatch uniq ship.engineで文字列内部への参照を取ったあとother_ship.engine := ImpulseEngine 0x42で同じエンジンを別のバリアントに切り替え- 続けて既存の
strを変更すると、コンテナが破壊された後に内部を使う問題が生じる
- そのためAnteは、可変借用参照がユニオンを指しているとき、そのバリアントの1つに対する可変借用参照を作れないようにする必要がある
- これは構造体の規則とは逆である
- 構造体への
mut参照があれば、フィールドへのmut参照を作れる - ユニオンへの
mut参照があっても、バリアント内部へのmut参照は作れない
- 構造体への
uniqとtemporary uniq conversion
uniqはexclusive mutable reference、つまり排他的可変参照を意味する- ある変数が
uniq Spaceshipを保持しているなら、それはそのSpaceshipに対して唯一利用可能な参照である- Rustの
&mut Spaceshipに近い概念である
- Rustの
- ユニオン内部を安全に扱うために、Anteはtemporary uniq conversionを使う
- 中心的な規則は、特定のスコープでほかのエイリアス可能な参照を使わないなら、一時的に
uniq参照を得られるというものであるmatch uniq ship.engineの区間では、ship.engineに対してuniqのようにアクセスする- この区間のあいだ、コンパイラは
Spaceshipを間接的に含みうるほかの既存変数を使えないようにする
- Rustは「ほかの参照がどこかに存在するかもしれない」という理由で
uniqの存在自体を認めないのに対し、Anteはそのスコープ内でそれらの参照を使わないという条件でuniqを許可する - このとき
uniq Spaceshipは実際にグローバルに唯一の参照なのではなく、そのスコープ内で唯一利用可能な参照である- Cの
restrictポインタに近いニュアンスを持つ
- Cの
許可されるアクセスと拒否されるアクセス
match uniq ship.engineのスコープ内でother_ship: Rc Spaceshipにアクセスすると、コンパイルエラーになるべきであるother_ship.engineがship.engineとaliasしている可能性がありship.engineを使っているあいだにother_ship.engineの変更がdropを引き起こしうるためである
HasAShipのようにRc Spaceshipをフィールドに持つ別の構造体も同じ理由で拒否されるother.ship.engineも間接的に同じSpaceshipへ到達しうるためである
- 一方で
new_fuel: I32のような整数は使えるI32はSpaceshipへの参照を含みえないためである
Spaceship自体がfollow_ship: Rc Spaceshipのようなフィールドを含む場合も拒否される- その場合
uniq Spaceshipも自身の内部経路を通じて再到達可能になってしまうため、一般に再帰型ではmut -> uniq変換はできない
- その場合
関数呼び出しと返り値での制約
- 関数呼び出しでも
mut -> uniq変換が起こりうる foo (var ship: Rc Spaceship) (new_res: Resonator)がmaybe_use_resonator ship new_resを呼ぶとき、呼び出し地点でshipはuniq Spaceshipへ変換される- コンパイラは、ほかの引数が
Spaceship参照を含みうるかどうかだけを確認すればよい - 例の
Resonatorはそのような参照を含まないため許可される
- コンパイラは、ほかの引数が
- 返り値では、変換された
uniq参照を通常のuniqとして返すことはできない- 返却後には、「スコープ内でalias可能な変数を使わない」というコンパイラ検査が適用されないためである
- 代わりに返り値型を
local uniq Fooと指定できる- 内部的には
mut refからuniq refへ変換すると、実際には常にlocal uniqが作られる - ほとんどの場合は通常の
uniqのように使えるが、返すときには明示が必要である
- 内部的には
設計上のコストと代替案
- Anteは
Rc Spaceshipのような参照カウント参照を、ランタイムエラーなしで一時的なuniq Spaceshipへ変換できる - 欠点は、コンパイラが「
EngineからSpaceshipへ到達できるか」といった問いに答えるため、型を再帰的に調べる必要がある点である - こうした解析は脆弱になりうる
- 構造体へのフィールド追加がbreaking API changeになりうる
- Anteの作者Jakeは、この保証を維持するより良い方法を探している
- group borrowingやFlix referencesのように、各共有可変型に匿名の一意ブランド型を付ける方式
- 共有型を変更するときに
Mutates 'aのようなeffectを追加して型解析を不要にする方式 - 2つの参照が別オブジェクトを指すかを利用者がランタイムで検査する、またはsafe APIで包んだunsafe検査を提供する方式
- コンパイラが
Rc内部に間接保存されておらずaliasしえない値を追跡する方式
- Ponyのiso permissionに似たアイデアや、構造体内部は見ても外を指す参照は使えないようにする一時的権限も候補として残っている
- 難しいのは、こうした柔軟性を保ちながら、Anteの目標である使いやすさ、可読性、単純さを守ることである
より広いメモリ安全性の潮流
- shared mutable borrowingは以前は不可能だと考えられており、Rustもそうした前提の上に設計されたという見方がある
- いくつもの例外が積み重なってきている
- Anteはlocal uniqueness規則により、shared-mutableデータから
uniq借用参照を得られる - Valeはpure functionを通じて、shared-mutableデータから不変借用参照を得られる
- group borrowingはshape-stableでなくてもshared-mutable借用参照を作れる
- RustのGhostCellはオブジェクトグラフ同士が自由に相互参照できるようにしつつ、特定時点ではそのうち1つに対する可変参照1本だけを持てる
- Anteはlocal uniqueness規則により、shared-mutableデータから
- この流れは、メモリ安全性設計においてshared mutable borrowingを扱う、より一般的な原理が存在する可能性を示唆している
Rust Cellとの比較
- Rust利用者は、構造体フィールドに
Cellを入れる方式とAnteのアプローチの違いを尋ねるかもしれない - Anteの例では、
Rc Spaceshipからstatus: Stringへのmut String参照を得て、" (refueling)"を直接追加できる - Rustの
Cell<String>方式では、Rc<Spaceship>から&mut Stringを得ることはできない- 代わりに
status_ref.replace(String::new())で一時的なデフォルト値を入れ - 取り出した
Stringを変更し - 最後に
replace(status)で戻さなければならない
- 代わりに
- この方式にはいくつかの欠点がある
""のようなデフォルトインスタンスを作る必要がある- 最後の
replace呼び出しを忘れるリスクがある - 値が置き換えられた状態で誰かが
statusを読むリスクがある
- Anteは、一時的に
status文字列への参照を取得できるようにし、そのあいだ他のコードがアクセスできないことをコンパイラに強制させる
1件のコメント
Lobste.rs の意見
「共有された可変借用」が不可能だと考えられていたのは、単に Rust が目標を達成するために受け入れた犠牲だったのではなく、Rust の中核的な目標そのものに近い
共有された可変状態は、コードについての 局所的推論 を難しくするからだ
"References are like jumps" by withoutboats はこの点をうまく扱っている。エイリアスのある状態を偶発的に変更できないようにすることが、正しく動作するシステムを作りやすくするための核心であり、Rust のライフタイム規則は単にガベージコレクションを避けるための仕組みではなく、可変状態とエイリアス状態を同時に許す言語で推論可能性を確保するための、より深い構造だという主張だ
かなり良さそうに見える
理解が正しければ、共有参照から可変参照へ移る魔法は、スレッド間で共有されない型に限定されているから可能で、
Rcの一意性は、同じ型のすべてのオブジェクトが同じライフタイムで借用されているかのように扱うことで保証しているようだ明示的な構文と自然な構文のどちらがよいかは好みの問題かもしれないが、コンパイラが
Cellについてもっと多くを知っていれば、それに対する可変参照をより柔軟に許可できることを示しているそして Rust で
mutが可変ではなく 排他的/一意 を意味するかのように使われる、紛らわしい用語も避けているuniqへの昇格はロック取得を意味するのか?」という疑問だったが、比較対象がArcではなくRcだという意味だと理解したmutが排他的/一意を意味するという部分を、もう少し説明してもらえる?最後のほうで示唆されていた 統一原理 が何なのか、見当がつく人がいるのか気になる
antelang.org のブログ記事に関する以前の議論 も参考になる
これがどう動くのか、よくわからない。「オブジェクトへの可変ポインタがあれば、そのオブジェクトのスライスへの変更参照を得られる」という意味に見える
しかしそうだとすると、たとえば
mutref someobjext = …、mutref subfield = someobjext.a.b、someobjext.a = somethingelseのようなことができそうで、するとsubfieldは無効になるか、値が変わって壊れてしまうかもしれない記事には説明や他言語との比較、コード例は多かったが、肝心のこの動作の 段階的なセマンティクス を基礎から整理した部分は見つけにくかった