Skip to content

Commit

Permalink
Merge pull request #481 from nikomatsakis/struct-field-ingredients
Browse files Browse the repository at this point in the history
Struct field ingredients
  • Loading branch information
nikomatsakis authored Apr 2, 2024
2 parents 9a0a651 + 8772961 commit b5aa429
Show file tree
Hide file tree
Showing 33 changed files with 793 additions and 298 deletions.
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Plumbing](./plumbing.md)
- [Jars and ingredients](./plumbing/jars_and_ingredients.md)
- [Databases and runtime](./plumbing/database_and_runtime.md)
- [Tracked structures](./plumbing/tracked_structs.md)
- [Query operations](./plumbing/query_ops.md)
- [maybe changed after](./plumbing/maybe_changed_after.md)
- [Fetch](./plumbing/fetch.md)
Expand Down
50 changes: 50 additions & 0 deletions book/src/plumbing/tracked_structs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Tracked structs

Tracked structs are stored in a special way to reduce their costs.

Tracked structs are created via a `new` operation.

## The tracked struct and tracked field ingredients

For a single tracked struct we create multiple ingredients.
The **tracked struct ingredient** is the ingredient created first.
It offers methods to create new instances of the struct and therefore
has unique access to the interner and hashtables used to create the struct id.
It also shares access to a hashtable that stores the `TrackedStructValue` that
contains the field data.

For each field, we create a **tracked field ingredient** that moderates access
to a particular field. All of these ingredients use that same shared hashtable
to access the `TrackedStructValue` instance for a given id. The `TrackedStructValue`
contains both the field values but also the revisions when they last changed value.

## Each tracked struct has a globally unique id

This will begin by creating a *globally unique, 32-bit id* for the tracked struct. It is created by interning a combination of

* the currently executing query;
* a u64 hash of the `#[id]` fields;
* a *disambiguator* that makes this hash unique within the current query. i.e., when a query starts executing, it creates an empty map, and the first time a tracked struct with a given hash is created, it gets disambiguator 0. The next one will be given 1, etc.

## Each tracked struct has a `TrackedStructValue` storing its data

The struct and field ingredients share access to a hashmap that maps
each field id to a value struct:

```rust,ignore
{{#include ../../../components/salsa-2022/src/tracked_struct.rs:TrackedStructValue}}
```

The value struct stores the values of the fields but also the revisions when
that field last changed. Each time the struct is recreated in a new revision,
the old and new values for its fields are compared and a new revision is created.

## The macro generates the tracked struct `Configuration`

The "configuration" for a tracked struct defines not only the types of the fields,
but also various important operations such as extracting the hashable id fields
and updating the "revisions" to track when a field last changed:

```rust,ignore
{{#include ../../../components/salsa-2022/src/tracked_struct.rs:Configuration}}
```
78 changes: 7 additions & 71 deletions components/salsa-2022-macros/src/salsa_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,8 @@
//! * data method `impl Foo { fn data(&self, db: &dyn crate::Db) -> FooData { FooData { f: self.f(db), ... } } }`
//! * this could be optimized, particularly for interned fields

use crate::{
configuration,
options::{AllowedOptions, Options},
};
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use crate::options::{AllowedOptions, Options};
use proc_macro2::{Ident, Span, TokenStream};
use syn::spanned::Spanned;

pub(crate) enum SalsaStructKind {
Expand Down Expand Up @@ -217,69 +213,6 @@ impl<A: AllowedOptions> SalsaStruct<A> {
}
}

/// For each of the fields passed as an argument,
/// generate a struct named `Ident_Field` and an impl
/// of `salsa::function::Configuration` for that struct.
pub(crate) fn field_config_structs_and_impls<'a>(
&self,
fields: impl Iterator<Item = &'a SalsaField>,
) -> (Vec<syn::ItemStruct>, Vec<syn::ItemImpl>) {
let ident = &self.id_ident();
let jar_ty = self.jar_ty();
let visibility = self.visibility();
fields
.map(|ef| {
let value_field_name = ef.name();
let value_field_ty = ef.ty();
let value_field_backdate = ef.is_backdate_field();
let config_name = syn::Ident::new(
&format!(
"__{}",
format!("{}_{}", ident, value_field_name).to_upper_camel_case()
),
value_field_name.span(),
);
let item_struct: syn::ItemStruct = parse_quote! {
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
#visibility struct #config_name(std::convert::Infallible);
};

let execute_string = Literal::string(&format!("`execute` method for field `{}::{}` invoked",
ident,
ef.name(),
));

let recover_from_cycle_string = Literal::string(&format!("`execute` method for field `{}::{}` invoked",
ident,
ef.name(),
));

let should_backdate_value_fn = configuration::should_backdate_value_fn(value_field_backdate);
let item_impl: syn::ItemImpl = parse_quote! {
impl salsa::function::Configuration for #config_name {
type Jar = #jar_ty;
type SalsaStruct = #ident;
type Key = #ident;
type Value = #value_field_ty;
const CYCLE_STRATEGY: salsa::cycle::CycleRecoveryStrategy = salsa::cycle::CycleRecoveryStrategy::Panic;

#should_backdate_value_fn

fn execute(db: &salsa::function::DynDb<Self>, key: Self::Key) -> Self::Value {
panic!(#execute_string)
}

fn recover_from_cycle(db: &salsa::function::DynDb<Self>, cycle: &salsa::Cycle, key: Self::Key) -> Self::Value {
panic!(#recover_from_cycle_string)
}
}
};

(item_struct, item_impl)
})
.unzip()
}

/// Generate `impl salsa::AsId for Foo`
pub(crate) fn as_id_impl(&self) -> syn::ItemImpl {
let ident = self.id_ident();
Expand Down Expand Up @@ -307,7 +240,6 @@ impl<A: AllowedOptions> SalsaStruct<A> {
// `::salsa::debug::helper::SalsaDebug` will use `DebugWithDb` or fallbak to `Debug`
let fields = self
.all_fields()
.into_iter()
.map(|field| -> TokenStream {
let field_name_string = field.name().to_string();
let field_getter = field.get_name();
Expand Down Expand Up @@ -405,7 +337,7 @@ impl SalsaField {
if BANNED_FIELD_NAMES.iter().any(|n| *n == field_name_str) {
return Err(syn::Error::new(
field_name.span(),
&format!(
format!(
"the field name `{}` is disallowed in salsa structs",
field_name_str
),
Expand Down Expand Up @@ -435,6 +367,10 @@ impl SalsaField {
Ok(result)
}

pub(crate) fn span(&self) -> Span {
self.field.span()
}

/// The name of this field (all `SalsaField` instances are named).
pub(crate) fn name(&self) -> &syn::Ident {
self.field.ident.as_ref().unwrap()
Expand Down
1 change: 1 addition & 0 deletions components/salsa-2022-macros/src/tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub(crate) fn tracked_impl(
),
None => format!("{}", self_type_name),
};
#[allow(clippy::manual_try_fold)] // we accumulate errors
let extra_impls = item_impl
.items
.iter_mut()
Expand Down
Loading

0 comments on commit b5aa429

Please sign in to comment.