Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Salsa の仕組み

この章は、Niko Matsakis による Salsa についてのこの 動画での説明に基づいています。 さらに詳しく知りたい場合は、同じく Niko Matsakis による Salsa In More Depth を見るとよいでしょう。

2022 年 11 月時点では、Salsa は rustc のクエリシステムに

(他のものとともに)触発されていますが、rustc で直接使われているわけではありません。 Rust のトレイトシステムの実装である chalk や、 Rust 向け Language Server Protocol の公式実装である rust-analyzer では 広く使われていますが、コンパイラに統合するための中期的または長期的な具体的な 計画はありません。

Salsa とは何か?

Salsa はインクリメンタルな再計算のためのライブラリです。これは、過去にすでに行われた 計算を再利用することで、将来の計算の効率を高められることを意味します。

Salsa の目的は次のとおりです。

  • その機能を自動的な形で提供し、古い計算の再利用を ライブラリによって自動的に行うこと。
  • それを「健全」または「正しい」方法で行い、その結果として 最初から行った場合と同じ結果を導くこと。

Salsa の実際のモデルははるかに豊かで、多くの種類の入力と多くの異なる出力を扱えます。 たとえば、Salsa を IDE と統合する場合、 入力はマニフェスト(Cargo.tomlrust-toolchain.toml)、ソースファイル全体 (foo.rs)、スニペットなどになり得ます。そのような統合の出力は、 バイナリ実行可能ファイルから、lint、型(たとえば、ユーザーが特定の変数を 選択してその型を見たい場合)、補完などまでさまざまです。

どのように機能するのか?

Salsa が最初に行う必要があるのは、計算されたものではなく入力として与えられる 「基底入力」を識別することです。

次に Salsa は、中間的な「派生」値も識別する必要があります。これは ライブラリが生成するものですが、各派生値について、その派生値を計算する 「純粋」関数が存在します。

たとえば、ast(x: Path) -> AST という関数があるかもしれません。生成される 抽象構文木(AST)は最終的な値ではなく、ライブラリが計算に使用する中間値です。

これは、ライブラリで計算しようとすると、Salsa がさまざまな派生値を 計算し、最終的に入力を読み取り、求められた計算の結果を生成することを意味します。

計算の過程で、Salsa はどの入力がアクセスされ、どの値が派生されたかを追跡します。 この情報は、入力が変更されたときに何が起こるか、つまり派生値がまだ有効かどうかを 判断するために使われます。

これは必ずしも、入力の下流にある各計算がチェックされることを意味しません。 それは高コストになり得ます。Salsa は、変更されていないものを見つけるまで、 各下流の計算をチェックするだけで済みます。その時点で、他の派生計算は 変更する必要がないため、チェックされません。

これは、ノードを持つグラフとして考えるとわかりやすいです。各派生値は 他の値に依存しており、それらは基底値または派生値のいずれかです。 基底値には依存関係がありません。

I <- A <- C ...
          |
J <- B <--+

入力 I が変化すると、派生値 A は変化する可能性があります。 派生値 BIA、または AI から派生したどの値にも 依存していないため、変更の対象ではありません。したがって、Salsa は B について過去に行った計算を、再計算することなく再利用できます。

計算は早期に終了することもあります。前と同じグラフのまま、 入力 I が何らかの形で変化した(そして入力 J は変化していない)とします。 しかし、A を再度計算したところ、A が前回の計算から変化していないことが 判明したとします。これは「早期終了」につながります。なぜなら、C の直接の入力である AB の両方が変化していないため、C が変更される必要があるかどうかを チェックする必要がないからです。

Salsa の主要な概念

クエリ

クエリとは、Salsa が計算の過程でアクセスできる何らかの値です。各 クエリは(0 個から多数までの)キーをいくつか持つことができ、すべてのクエリは 関数と同様に結果を持ちます。0-key クエリは「入力」クエリと呼ばれます。

データベース

データベースは基本的に計算全体のコンテキストであり、Salsa の内部状態、 各クエリのすべての中間値、そして計算に必要となるその他すべてのものを 保存するためのものです。データベースは、構築される前にライブラリが行う すべてのクエリを知っている必要がありますが、それらを同じ場所で指定する必要はありません。

データベースが形成されると、関数によく似たクエリでアクセスできます。 各クエリの結果はデータベースに保存されているため、クエリが N 回呼び出されると、 (入力が再計算を正当化するような形で変更されていない限り)クエリを再計算することなく、 N 個の clone された 結果を返します。

各入力クエリ(0-key)については、「set」メソッドが生成され、ユーザーは そのようなクエリの出力を変更し、以前にメモ化された値が無効化される可能性を 引き起こすことができます。

クエリグループ

クエリグループとは、1 つの単位としてまとめて定義されたクエリの集合です。 データベースはクエリグループを組み合わせることで形成されます。クエリグループは 「Salsa モジュール」に似ています。

クエリグループ内のクエリの集合は、トレイト内のメソッドの集合にすぎません。

クエリグループを作成するには、特定の属性 (#[salsa::query_group(...)])で注釈付けされたトレイトを作成する必要があります。

その属性には引数も指定しなければなりません。これは、後でデータベースが作成されるときに 使用される struct を Salsa が作成するために使われます。

入力クエリグループの例:

/// この属性はこのツリーを処理し、このツリーを出力として生成し、Salsa も使用する
/// 中間的なものを大量に生成します。これらのうちの 1 つは
/// "StorageStruct" であり、その名前は属性で指定しています。
///
/// このクエリグループは、いかなる派生入力にも依存しない **入力** クエリの集まりです。
#[salsa::query_group(InputsStorage)]
pub trait Inputs {
    /// この属性(`#[salsa::input]`)は、このクエリが基底
    /// 入力であることを示すため、`set_manifest` が自動生成されます
    #[salsa::input]
    fn manifest(&self) -> Manifest;

    #[salsa::input]
    fn source_text(&self, name: String) -> String;
}

派生 クエリグループを作成するには、次の例に示すように、 このクエリグループがどの他のクエリグループに依存するかを、 それらをスーパートレイトとして指定することで指定しなければなりません。

/// このクエリグループには、派生値に依存するクエリが含まれます。
/// クエリグループは、依存関係をスーパートレイトとして指定することで、別の
/// クエリグループのクエリにアクセスできます。クエリグループは、このパターンを使って
/// 必要なだけ積み重ねることができます。
#[salsa::query_group(ParserStorage)]
pub trait Parser: Inputs {
    /// このクエリ `ast` は入力クエリではなく、派生クエリです。これは
    /// 定義が必要であることを意味します。
    fn ast(&self, name: String) -> String;
}

派生クエリを作成する場合、そのクエリの実装は trait の外部で定義する必要があります。定義は、 その他のキーに加えて、定義が属するクエリグループである trait を impl Trait(または dyn Trait)としてデータベースパラメーターに取る必要があります。

/// これは `Parser` trait における `ast` クエリの定義になります。
/// そのため、クエリ `ast` が呼び出され、再計算が必要になった場合、Salsa は
/// この関数を呼び出し、データベースを `impl Parser` として渡します。
/// この関数は、すべてのクエリグループのすべてのクエリを認識する必要はありません
fn ast(db: &impl Parser, name: String) -> String {
    //! なお、ここでは `impl Parser` が使用されていますが、`dyn Parser` でも同様に動作します
    /* code */
    ///`impl Parser` を渡すことで、これは許可されます
    let source_text = db.input_file(name);
    /* 実際のパースを行う */
    return ast;
}

最終的に、すべてのクエリグループが定義された後、struct を宣言することでデータベースを 作成できます。

どのクエリグループをデータベースの一部にするかを指定するには、attribute#[salsa::database(...)])を追加する必要があります。その attribute の引数は、 クエリグループの storages を指定する identifiers のリストです。

///この属性は、どのクエリグループをデータベースに含めるかを指定します
#[salsa::database(InputsStorage, ParserStorage)]
#[derive(Default)] //任意!
struct MyDatabase {
    ///このフィールドも 1 つ必要です
    runtime : salsa::Runtime<MyDatabase>,
}
///そして、この trait を実装する必要があります
impl salsa::Database for MyDatabase {
    fn salsa_runtime(&self) -> &salsa::Runtime<MyDatabase> {
        &self.runtime
    }
}

使用例:

fn main() {
    let db = MyDatabase::default();
    db.set_manifest(...);
    db.set_source_text(...);
    loop {
        db.ast(...); //結果を再利用します
        db.set_source_text(...);
    }
}