1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • 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 Colorshared 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呼び出しが可能である
    • healertargetが同じEntityを指していても、このコードではEntityを破壊できないため、2つの参照は有効なままである
  • 構造体そのものとそのフィールド、さらにフィールドのフィールドへの可変参照も同時に許可されうる
    • ship: mut Spaceshipengine_alias: mut Engine = ship.engineを同時に使っても、関数実行中にshipとその中のengineが破壊されないと判断する
  • RustやSwiftでは、同じデータを複数の&mut参照が同時に指す形は許可されない

参照カウントされた値のフィールド可変借用

  • Anteでは型定義の前にsharedを付けると、その型は自動的に参照カウントされる
  • shared mut type Spaceshipの例では、launchRcに相当するSpaceshipを保持しつつ、mut ship.engineset_fuelに渡す
  • launchが包含オブジェクトであるSpaceshipを保持しているため、そのフィールドであるengineも生存していると判断できる
  • 一般規則として、shared mut型のフィールドに対しては常にmut借用参照を作れる
    • ただし、そのフィールドの内部にあるすべての対象に対して常に可変借用を作れるわけではなく、別の規則が必要になる
  • 以後の例では、糖衣構文であるshared mut type Spaceshipの代わりに、より明示的なRc Spaceship表記を使う
    • shared mut type Spaceshiptype Spaceshipとなり、var ship: Spaceshipvar ship: Rc Spaceshipとなる

ユニオンが安全性の問題を生む箇所

  • ユニオンは内容をインラインで保持するため、ポインタ追跡やキャッシュミスを減らせ、速度の面で有利である
    • Cのunion Enginestruct Spaceshipの中に入ると、StringTheoryEngineImpulseEngineSpaceshipのメモリ内に配置される
    • Javaのようにインターフェースとポインタを使う方式と対比される
  • 問題は、メモリ安全言語でユニオンを安全にサポートするのが難しい点にある
  • EngineStringTheoryEngine(str: String)またはImpulseEngine(fuel: I32)である例では、shipother_shipが同じSpaceshipを指しているとセグメンテーションフォルトが起こりうる
    • match uniq ship.engineで文字列内部への参照を取ったあと
    • other_ship.engine := ImpulseEngine 0x42で同じエンジンを別のバリアントに切り替え
    • 続けて既存のstrを変更すると、コンテナが破壊された後に内部を使う問題が生じる
  • そのためAnteは、可変借用参照がユニオンを指しているとき、そのバリアントの1つに対する可変借用参照を作れないようにする必要がある
  • これは構造体の規則とは逆である
    • 構造体へのmut参照があれば、フィールドへのmut参照を作れる
    • ユニオンへのmut参照があっても、バリアント内部へのmut参照は作れない

uniqとtemporary uniq conversion

  • uniqexclusive mutable reference、つまり排他的可変参照を意味する
  • ある変数がuniq Spaceshipを保持しているなら、それはそのSpaceshipに対して唯一利用可能な参照である
    • Rustの&mut Spaceshipに近い概念である
  • ユニオン内部を安全に扱うために、Anteはtemporary uniq conversionを使う
  • 中心的な規則は、特定のスコープでほかのエイリアス可能な参照を使わないなら、一時的にuniq参照を得られるというものである
    • match uniq ship.engineの区間では、ship.engineに対してuniqのようにアクセスする
    • この区間のあいだ、コンパイラはSpaceshipを間接的に含みうるほかの既存変数を使えないようにする
  • Rustは「ほかの参照がどこかに存在するかもしれない」という理由でuniqの存在自体を認めないのに対し、Anteはそのスコープ内でそれらの参照を使わないという条件でuniqを許可する
  • このときuniq Spaceshipは実際にグローバルに唯一の参照なのではなく、そのスコープ内で唯一利用可能な参照である
    • Cのrestrictポインタに近いニュアンスを持つ

許可されるアクセスと拒否されるアクセス

  • match uniq ship.engineのスコープ内でother_ship: Rc Spaceshipにアクセスすると、コンパイルエラーになるべきである
    • other_ship.engineship.engineとaliasしている可能性があり
    • ship.engineを使っているあいだにother_ship.engineの変更がdropを引き起こしうるためである
  • HasAShipのようにRc Spaceshipをフィールドに持つ別の構造体も同じ理由で拒否される
    • other.ship.engineも間接的に同じSpaceshipへ到達しうるためである
  • 一方でnew_fuel: I32のような整数は使える
    • I32Spaceshipへの参照を含みえないためである
  • 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を呼ぶとき、呼び出し地点でshipuniq 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 borrowingFlix 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本だけを持てる
  • この流れは、メモリ安全性設計において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件のコメント

 
GN⁺ 4 시간 전
Lobste.rs の意見
  • 共有された可変借用」が不可能だと考えられていたのは、単に Rust が目標を達成するために受け入れた犠牲だったのではなく、Rust の中核的な目標そのものに近い
    共有された可変状態は、コードについての 局所的推論 を難しくするからだ
    "References are like jumps" by withoutboats はこの点をうまく扱っている。エイリアスのある状態を偶発的に変更できないようにすることが、正しく動作するシステムを作りやすくするための核心であり、Rust のライフタイム規則は単にガベージコレクションを避けるための仕組みではなく、可変状態とエイリアス状態を同時に許す言語で推論可能性を確保するための、より深い構造だという主張だ

    • 以前に筆者が Mojo borrow checker を扱ったときにも同じことを感じた。Rust の借用チェッカーは、単一スレッドのプログラムでも 値セマンティクス を保ってくれる
  • かなり良さそうに見える
    理解が正しければ、共有参照から可変参照へ移る魔法は、スレッド間で共有されない型に限定されているから可能で、Rc の一意性は、同じ型のすべてのオブジェクトが同じライフタイムで借用されているかのように扱うことで保証しているようだ
    明示的な構文と自然な構文のどちらがよいかは好みの問題かもしれないが、コンパイラが Cell についてもっと多くを知っていれば、それに対する可変参照をより柔軟に許可できることを示している
    そして Rust で mut が可変ではなく 排他的/一意 を意味するかのように使われる、紛らわしい用語も避けている

    • スレッド間ではどうなるのか気になった。「uniq への昇格はロック取得を意味するのか?」という疑問だったが、比較対象が Arc ではなく Rc だという意味だと理解した
    • mut が排他的/一意を意味するという部分を、もう少し説明してもらえる?
  • 最後のほうで示唆されていた 統一原理 が何なのか、見当がつく人がいるのか気になる

  • antelang.org のブログ記事に関する以前の議論 も参考になる

  • これがどう動くのか、よくわからない。「オブジェクトへの可変ポインタがあれば、そのオブジェクトのスライスへの変更参照を得られる」という意味に見える
    しかしそうだとすると、たとえば mutref someobjext = …mutref subfield = someobjext.a.bsomeobjext.a = somethingelse のようなことができそうで、すると subfield は無効になるか、値が変わって壊れてしまうかもしれない
    記事には説明や他言語との比較、コード例は多かったが、肝心のこの動作の 段階的なセマンティクス を基礎から整理した部分は見つけにくかった