Jar 和配料

⚠️ Salsa 仍在进展中 ⚠️

本页描述了未发布的 Salsa 2022 版本,该版本与旧版本大不相同。此处的代码有效,但仅在 github 和 salsa-2022 crate 中可用。

本节介绍 Salsa 中的数据是如何组织的,以及 Salsa 条目(items) 之间如何联系起来(例如,依赖条目跟踪)。

Salsa 条目和配料

Salsa 条目是指可以包括在 Jar 中、带有 Salsa 属性宏标注的条目。例如,跟踪函数是一个 Salsa 条目:

#[salsa::tracked] fn foo(db: &dyn Db, input: MyInput) { }

... Salsa 输入也是如此...

#[salsa::input] struct MyInput { }

...或跟踪结构:

#[salsa::tracked] struct MyStruct { }

每个 Salsa 条目在运行时都需要特定的数据才能运行。这些数据被称为 “配料” (ingredients)。

大多数 Salsa 条目只产生一种配料,但有时会产生不止一种配料。

例如,跟踪函数会生成一个 FunctionIngredient。但跟踪结构会生成几个配料,一个用于结构本身的 TrackedStructIngredient,一个用于每个值字段的 FunctionIngredient

配料定义了核心逻辑

大多数有趣的 Salsa 代码都存在于这些配料中。例如,当你创建新的跟踪结构,会调用方法 TrackedStruct::new_struct,它负责确定跟踪结构的 id。

类似地,当你调用一个跟踪函数,它被转换为对 TrackedFunction::fetch 的调用,从而决定是否有有效的记忆值要返回,或者该函数是否必须执行。

Ingredient trait

每个配料都实现了 Ingredient<DB> trait ,该 trait 定义了各种配料支持的泛型操作。

例如,可以使用 maybe_changed_after 方法来检查自给定版本以来,存储在配料中的某些特定数据是否发生了更改:

下面我们将看到,每个数据库 DB 都能够获取一个 IngredientIndex,并用它来获取相应的配料 &dyn Ingredient<DB>

这允许数据库对已索引的配料执行泛型操作,而无需确切知道该配料的类型。

Jar 是各种配料的集合

当你声明一个 Salsa 的 jar 时,需列出了该 jar 中包含的每个 Salsa 条目:

#[salsa::jar] struct Jar( foo, MyInput, MyStruct );

这将展开成如下所示的结构体:

struct Jar( <foo as IngredientsFor>::Ingredient, <MyInput as IngredientsFor>::Ingredient, <MyStruct as IngredientsFor>::Ingredient, )

IngredientsFor trait 用于定义某些 Salsa 条目所需的配料,例如跟踪函数 foo 或跟踪结构 MyInput

每个 Salsa 条目都定义了一个类型 I,因此 <I as IngredientsFor>::Ingredient 提供了 I 所需的配料。

数据库是一组 Jars

Salsa 的数据库存储最终归结为一组 Jars 结构体,其中每个 Jar 结构体本身包含 Salsa 条目的配料。

因此,数据库可以被认为是一个配料列表,尽管该列表被组织成两级层次结构。

这种两级层次结构的原因是它允许单独的编译和私有化。

罗列 Jars 的 crate 不必知道 Jar 的内容即可将 Jar 结构体嵌入到数据库中。 Jar 中出现的某些类型可能是另一个结构体的私有类型。

HasJars trait 和 Jars 类型

每个 Salsa 数据库都实现了 HasJars trait,该 trait 由 salsa::db 过程宏生成。

此外,HarJars trait 还定义了一个映射到 trait 中的 Jars 元组的 Jars 关联类型。

例如,给定一个这样的数据库...

#[salsa::db(Jar1, ..., JarN)] struct MyDatabase { storage: salsa::Storage<Self> }

salsa::db 宏将生成一个包含 type Jars = (Jar1, ..., JarN)HasJars 实现。

impl salsa::storage::HasJars for #db { type Jars = (#(#jar_paths,)*);

反过来说,salsa::Storage<DB> 类型最终包含一个嵌入 DB::JarsShared 结构体,从而嵌入每个 Jar 的所有数据。

配料索引

在初始化期间,数据库中的每个配料都被分配了一个唯一的索引,称为 IngredientIndex。这是一个 32 位数字,用于标识特定 Jar 中的特定配料。

路线

除了索引,数据库中的每一种配料也有对应的 路线 (route)。

路线是一个闭包,它在给定一个 DB::Jars 元组引用的情况下,返回 &dyn Ingredient<DB>

路线表允许我们从特定配料的 IngredientIndex 转到它的 &dyn Ingredient<DB> trait 对象。如稍后所述,在初始化数据库时会创建路线表。

数据库键和依赖项键

DatabaseKeyIndex 标识了存储在特定配料中的特定值。它结合了 IngredientIndexkey_index,其中 key_indexsalsa::Id

/// An "active" database key index represents a database key index /// that is actively executing. In that case, the `key_index` cannot be /// None. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct DatabaseKeyIndex { pub(crate) ingredient_index: IngredientIndex, pub(crate) key_index: Id, }

DependencyIndex 类似,但 key_index 是可选的。有时当我们希望引用整个配料而不是配料中的任何特定值时,可以使用这种方法。

这些类型的索引用于存储配料之间的联系。例如,每个记录的值都必须跟踪其输入。这些输入被存储为 DependencyIndex

然后我们可以做一些事情,比如通过以下途径知道“这个输入自修订 R 以来是否发生了变化”:

  • 使用配料索引查找路线并获取 &dyn Ingredient<DB>
  • 然后对该 trait 对象调用 maybe_changed_since 方法

HasJarsDyn

在上面的设置有一个问题。使用者的代码总是与 dyn crate::Db 值交互,其中 crate::Db 是 Jar 定义的 trait。

crate::Db trait 扩展了 salsa::HasJar,而后者又扩展了 salsa::Database

理想情况下,我们应该让 salsa::Database 扩展 salsa::HasJars,这是访问 Jars 数据的主要 trait 。

但我们不想这样做,因为 HasJars 定义了一个关联的类型 Jars,这意味着每个对 dyn crate::Db 的引用都必须使用类似于 dyn crate::Db<Jars = J> 的内容来指定 Jars 类型。这将是不符合人体工程学的,但更糟糕的是,这实际上是不可能的:最终的 Jars 类型结合了来自多个 crates 的 Jars,因此任何一个单独的 Jar crate 都不知道。

为了解决这个问题,salsa::Database 实际上扩展了另一个 trait HasJarsDyn,它没有直接显示 Jars 或配料类型,只是提供了可以对配料执行的各种方法,给出了它的 IngredientIndex

Ingredient<DB> 这样的 trait 需要了解完整的 DB 类型。如果一个函数配料直接调用 Ingredient<DB> 上的方法,这意味着它必须完全是泛型的,并且只有在整个数据库类型可用时才在最终的 crates 中实例化。

我们通过 HasJarsDyn trait 来解决这个问题。HasJarsDyn trait 导出一个将“查找配料,调用方法”的步骤合并起来的方法:

/// Dyn friendly subset of HasJars pub trait HasJarsDyn { fn runtime(&self) -> &Runtime; fn runtime_mut(&mut self) -> &mut Runtime; fn maybe_changed_after(&self, input: DependencyIndex, revision: Revision) -> bool; fn cycle_recovery_strategy(&self, input: IngredientIndex) -> CycleRecoveryStrategy; fn origin(&self, input: DatabaseKeyIndex) -> Option<QueryOrigin>; fn mark_validated_output(&self, executor: DatabaseKeyIndex, output: DependencyIndex); /// Invoked when `executor` used to output `stale_output` but no longer does. /// This method routes that into a call to the [`remove_stale_output`](`crate::ingredient::Ingredient::remove_stale_output`) /// method on the ingredient for `stale_output`. fn remove_stale_output(&self, executor: DatabaseKeyIndex, stale_output: DependencyIndex); /// Informs `ingredient` that the salsa struct with id `id` has been deleted. /// This means that `id` will not be used in this revision and hence /// any memoized values keyed by that struct can be discarded. /// /// In order to receive this callback, `ingredient` must have registered itself /// as a dependent function using /// [`SalsaStructInDb::register_dependent_fn`](`crate::salsa_struct::SalsaStructInDb::register_dependent_fn`). fn salsa_struct_deleted(&self, ingredient: IngredientIndex, id: Id); fn fmt_index(&self, index: DependencyIndex, fmt: &mut fmt::Formatter<'_>) -> fmt::Result; }

因此,从技术上讲,要检查一个输入是否发生了变化,一种配料:

  • dyn Database 上调用 HasJarsDyn::maybe_changed_after
  • 该方法由 #[salsa::db] 生成实现:
    • 从配料索引中获取配料的路径
    • 使用该路径获取该配料的 &dyn Ingredient
    • 在该配料上调用 maybe_changed_after

初始化数据库

最后要讨论的是数据库是如何初始化的。Storage<DB>Default 实现是这样的:

impl<DB> Default for Storage<DB> where DB: HasJars, { fn default() -> Self { let mut routes = Routes::new(); let jars = DB::create_jars(&mut routes); Self { shared: Arc::new(Shared { jars, cvar: Default::default(), }), routes: Arc::new(routes), runtime: Runtime::default(), } } }

首先,它创建一个空的 Routes 实例。然后调用 DB::create_jars 方法,此方法的实现由 #[salsa::db] 宏定义;它只是在每个 Jar 上调用 Jar::create_jar 方法:

fn create_jars(routes: &mut salsa::routes::Routes<Self>) -> Self::Jars { ( ( <#jar_paths as salsa::jar::Jar>::create_jar(routes), )* ) }

create_jar 这个实现由 #[salsa::jar] 宏生成,它只是遍历每个 Salsa 条目的所表示的类型,并要求它创建其配料

quote! { impl<'salsa_db> salsa::jar::Jar<'salsa_db> for #jar_struct { type DynDb = dyn #jar_trait + 'salsa_db; fn create_jar<DB>(routes: &mut salsa::routes::Routes<DB>) -> Self where DB: salsa::storage::JarFromJars<Self> + salsa::storage::DbWithJar<Self>, { ( let #field_var_names = <#field_tys as salsa::storage::IngredientsFor>::create_ingredients(routes); )* Self(#(#field_var_names),*) } } }

为任何特定条目创建配料的代码由相关的宏生成(例如 #[salsa::traced]#[salsa::input]),但它始终遵循特定的结构。

要创建配料,首先调用 Routes::push,它创建指向该配料的路线,并为其分配一个 IngredientIndex。然后,调用 FunctionIngredient::new 这样的函数来创建结构。到配料的路线被定义为给定 DB::Jars 的闭包,该闭包可以找到特定配料的数据。