make tracked structs coarse-grained by default

This commit is contained in:
Ibraheem Ahmed 2025-01-16 20:57:23 -05:00
parent 88dc597fc9
commit c90c2f2e8a
23 changed files with 269 additions and 88 deletions

View file

@ -20,19 +20,36 @@ macro_rules! setup_tracked_struct {
// Field names
field_ids: [$($field_id:ident),*],
// Field names
field_getters: [$($field_getter_vis:vis $field_getter_id:ident),*],
// Tracked field names
tracked_ids: [$($tracked_id:ident),*],
// Tracked field names
tracked_getters: [$($tracked_getter_vis:vis $tracked_getter_id:ident),*],
// Untracked field names
untracked_getters: [$($untracked_getter_vis:vis $untracked_getter_id:ident),*],
// Field types, may reference `db_lt`
field_tys: [$($field_ty:ty),*],
// Tracked field types
tracked_tys: [$($tracked_ty:ty),*],
// Untracked field types
untracked_tys: [$($untracked_ty:ty),*],
// Indices for each field from 0..N -- must be unsuffixed (e.g., `0`, `1`).
field_indices: [$($field_index:tt),*],
// Indices of fields to be used for id computations
id_field_indices: [$($id_field_index:tt),*],
// Indices of tracked fields.
tracked_indices: [$($tracked_index:tt),*],
// A set of "field options". Each field option is a tuple `(maybe_clone, maybe_backdate)` where:
// Indices of untracked fields.
untracked_indices: [$($untracked_index:tt),*],
// A set of "field options" for each field.
//
// Each field option is a tuple `(maybe_clone, maybe_backdate)` where:
//
// * `maybe_clone` is either the identifier `clone` or `no_clone`
// * `maybe_backdate` is either the identifier `backdate` or `no_backdate`
@ -41,6 +58,12 @@ macro_rules! setup_tracked_struct {
// (see e.g. @maybe_clone below).
field_options: [$($field_option:tt),*],
// A set of "field options" for each tracked field.
tracked_options: [$($tracked_option:tt),*],
// A set of "field options" for each untracked field.
untracked_options: [$($untracked_option:tt),*],
// Number of fields
num_fields: $N:literal,
@ -84,6 +107,10 @@ macro_rules! setup_tracked_struct {
$(stringify!($field_id),)*
];
const TRACKED_FIELD_DEBUG_NAMES: &'static [&'static str] = &[
$(stringify!($tracked_id),)*
];
type Fields<$db_lt> = ($($field_ty,)*);
type Revisions = $zalsa::Array<$Revision, $N>;
@ -98,8 +125,8 @@ macro_rules! setup_tracked_struct {
s.0
}
fn id_fields(fields: &Self::Fields<'_>) -> impl std::hash::Hash {
( $( &fields.$id_field_index ),* )
fn untracked_fields(fields: &Self::Fields<'_>) -> impl std::hash::Hash {
( $( &fields.$untracked_index ),* )
}
fn new_revisions(current_revision: $Revision) -> Self::Revisions {
@ -133,6 +160,7 @@ macro_rules! setup_tracked_struct {
pub fn ingredient(db: &dyn $zalsa::Database) -> &$zalsa_struct::IngredientImpl<$Configuration> {
static CACHE: $zalsa::IngredientCache<$zalsa_struct::IngredientImpl<$Configuration>> =
$zalsa::IngredientCache::new();
CACHE.get_or_create(db, || {
db.zalsa().add_or_lookup_jar_by_type(&<$zalsa_struct::JarImpl::<$Configuration>>::default())
})
@ -199,17 +227,33 @@ macro_rules! setup_tracked_struct {
}
$(
$field_getter_vis fn $field_getter_id<$Db>(self, db: &$db_lt $Db) -> $crate::maybe_cloned_ty!($field_option, $db_lt, $field_ty)
$tracked_getter_vis fn $tracked_getter_id<$Db>(self, db: &$db_lt $Db) -> $crate::maybe_cloned_ty!($tracked_option, $db_lt, $tracked_ty)
where
// FIXME(rust-lang/rust#65991): The `db` argument *should* have the type `dyn Database`
$Db: ?Sized + $zalsa::Database,
{
let db = db.as_dyn_database();
let fields = $Configuration::ingredient(db).field(db, self, $field_index);
let fields = $Configuration::ingredient(db).tracked_field(db, self, $tracked_index);
$crate::maybe_clone!(
$field_option,
$field_ty,
&fields.$field_index,
$tracked_option,
$tracked_ty,
&fields.$tracked_index,
)
}
)*
$(
$untracked_getter_vis fn $untracked_getter_id<$Db>(self, db: &$db_lt $Db) -> $crate::maybe_cloned_ty!($untracked_option, $db_lt, $untracked_ty)
where
// FIXME(rust-lang/rust#65991): The `db` argument *should* have the type `dyn Database`
$Db: ?Sized + $zalsa::Database,
{
let db = db.as_dyn_database();
let fields = $Configuration::ingredient(db).untracked_field(db, self, $untracked_index);
$crate::maybe_clone!(
$untracked_option,
$untracked_ty,
&fields.$untracked_index,
)
}
)*

View file

@ -64,7 +64,7 @@ impl crate::options::AllowedOptions for InputStruct {
impl SalsaStructAllowedOptions for InputStruct {
const KIND: &'static str = "input";
const ALLOW_ID: bool = false;
const ALLOW_TRACKED: bool = false;
const HAS_LIFETIME: bool = false;

View file

@ -65,7 +65,7 @@ impl crate::options::AllowedOptions for InternedStruct {
impl SalsaStructAllowedOptions for InternedStruct {
const KIND: &'static str = "interned";
const ALLOW_ID: bool = false;
const ALLOW_TRACKED: bool = false;
const HAS_LIFETIME: bool = true;

View file

@ -41,8 +41,8 @@ pub(crate) trait SalsaStructAllowedOptions: AllowedOptions {
/// The kind of struct (e.g., interned, input, tracked).
const KIND: &'static str;
/// Are `#[id]` fields allowed?
const ALLOW_ID: bool;
/// Are `#[tracked]` fields allowed?
const ALLOW_TRACKED: bool;
/// Does this kind of struct have a `'db` lifetime?
const HAS_LIFETIME: bool;
@ -57,7 +57,7 @@ pub(crate) trait SalsaStructAllowedOptions: AllowedOptions {
pub(crate) struct SalsaField<'s> {
field: &'s syn::Field,
pub(crate) has_id_attr: bool,
pub(crate) has_tracked_attr: bool,
pub(crate) has_default_attr: bool,
pub(crate) has_ref_attr: bool,
pub(crate) has_no_eq_attr: bool,
@ -69,7 +69,7 @@ const BANNED_FIELD_NAMES: &[&str] = &["from", "new"];
#[allow(clippy::type_complexity)]
pub(crate) const FIELD_OPTION_ATTRIBUTES: &[(&str, fn(&syn::Attribute, &mut SalsaField))] = &[
("id", |_, ef| ef.has_id_attr = true),
("tracked", |_, ef| ef.has_tracked_attr = true),
("default", |_, ef| ef.has_default_attr = true),
("return_ref", |_, ef| ef.has_ref_attr = true),
("no_eq", |_, ef| ef.has_no_eq_attr = true),
@ -105,7 +105,7 @@ where
fields,
};
this.maybe_disallow_id_fields()?;
this.maybe_disallow_tracked_fields()?;
this.maybe_disallow_default_fields()?;
this.check_generics()?;
@ -129,24 +129,24 @@ where
}
}
/// Disallow `#[id]` attributes on the fields of this struct.
/// Disallow `#[tracked]` attributes on the fields of this struct.
///
/// If an `#[id]` field is found, return an error.
/// If an `#[tracked]` field is found, return an error.
///
/// # Parameters
///
/// * `kind`, the attribute name (e.g., `input` or `interned`)
fn maybe_disallow_id_fields(&self) -> syn::Result<()> {
if A::ALLOW_ID {
fn maybe_disallow_tracked_fields(&self) -> syn::Result<()> {
if A::ALLOW_TRACKED {
return Ok(());
}
// Check if any field has the `#[id]` attribute.
// Check if any field has the `#[tracked]` attribute.
for ef in &self.fields {
if ef.has_id_attr {
if ef.has_tracked_attr {
return Err(syn::Error::new_spanned(
ef.field,
format!("`#[id]` cannot be used with `#[salsa::{}]`", A::KIND),
format!("`#[tracked]` cannot be used with `#[salsa::{}]`", A::KIND),
));
}
}
@ -199,25 +199,34 @@ where
.collect()
}
pub(crate) fn tracked_ids(&self) -> Vec<&syn::Ident> {
self.tracked_fields_iter()
.map(|(_, f)| f.field.ident.as_ref().unwrap())
.collect()
}
pub(crate) fn field_indices(&self) -> Vec<Literal> {
(0..self.fields.len())
.map(Literal::usize_unsuffixed)
.collect()
}
pub(crate) fn num_fields(&self) -> Literal {
Literal::usize_unsuffixed(self.fields.len())
pub(crate) fn tracked_indices(&self) -> Vec<Literal> {
self.tracked_fields_iter()
.map(|(index, _)| Literal::usize_unsuffixed(index))
.collect()
}
pub(crate) fn id_field_indices(&self) -> Vec<Literal> {
self.fields
.iter()
.zip(0..)
.filter_map(|(f, index)| if f.has_id_attr { Some(index) } else { None })
.map(Literal::usize_unsuffixed)
pub(crate) fn untracked_indices(&self) -> Vec<Literal> {
self.untracked_fields_iter()
.map(|(index, _)| Literal::usize_unsuffixed(index))
.collect()
}
pub(crate) fn num_fields(&self) -> Literal {
Literal::usize_unsuffixed(self.fields.len())
}
pub(crate) fn required_fields(&self) -> Vec<TokenStream> {
self.fields
.iter()
@ -237,10 +246,34 @@ where
self.fields.iter().map(|f| &f.field.vis).collect()
}
pub(crate) fn tracked_vis(&self) -> Vec<&syn::Visibility> {
self.tracked_fields_iter()
.map(|(_, f)| &f.field.vis)
.collect()
}
pub(crate) fn untracked_vis(&self) -> Vec<&syn::Visibility> {
self.untracked_fields_iter()
.map(|(_, f)| &f.field.vis)
.collect()
}
pub(crate) fn field_getter_ids(&self) -> Vec<&syn::Ident> {
self.fields.iter().map(|f| &f.get_name).collect()
}
pub(crate) fn tracked_getter_ids(&self) -> Vec<&syn::Ident> {
self.tracked_fields_iter()
.map(|(_, f)| &f.get_name)
.collect()
}
pub(crate) fn untracked_getter_ids(&self) -> Vec<&syn::Ident> {
self.untracked_fields_iter()
.map(|(_, f)| &f.get_name)
.collect()
}
pub(crate) fn field_setter_ids(&self) -> Vec<&syn::Ident> {
self.fields.iter().map(|f| &f.set_name).collect()
}
@ -256,6 +289,18 @@ where
self.fields.iter().map(|f| &f.field.ty).collect()
}
pub(crate) fn tracked_tys(&self) -> Vec<&syn::Type> {
self.tracked_fields_iter()
.map(|(_, f)| &f.field.ty)
.collect()
}
pub(crate) fn untracked_tys(&self) -> Vec<&syn::Type> {
self.untracked_fields_iter()
.map(|(_, f)| &f.field.ty)
.collect()
}
pub(crate) fn field_indexed_tys(&self) -> Vec<syn::Ident> {
self.fields
.iter()
@ -265,29 +310,18 @@ where
}
pub(crate) fn field_options(&self) -> Vec<TokenStream> {
self.fields
.iter()
.map(|f| {
let clone_ident = if f.has_ref_attr {
syn::Ident::new("no_clone", Span::call_site())
} else {
syn::Ident::new("clone", Span::call_site())
};
self.fields.iter().map(SalsaField::options).collect()
}
let backdate_ident = if f.has_no_eq_attr {
syn::Ident::new("no_backdate", Span::call_site())
} else {
syn::Ident::new("backdate", Span::call_site())
};
pub(crate) fn tracked_options(&self) -> Vec<TokenStream> {
self.tracked_fields_iter()
.map(|(_, f)| f.options())
.collect()
}
let default_ident = if f.has_default_attr {
syn::Ident::new("default", Span::call_site())
} else {
syn::Ident::new("required", Span::call_site())
};
quote!((#clone_ident, #backdate_ident, #default_ident))
})
pub(crate) fn untracked_options(&self) -> Vec<TokenStream> {
self.untracked_fields_iter()
.map(|(_, f)| f.options())
.collect()
}
@ -298,6 +332,20 @@ where
pub fn generate_lifetime(&self) -> bool {
self.args.no_lifetime.is_none()
}
fn tracked_fields_iter(&self) -> impl Iterator<Item = (usize, &SalsaField<'s>)> {
self.fields
.iter()
.enumerate()
.filter(|(_, f)| f.has_tracked_attr)
}
fn untracked_fields_iter(&self) -> impl Iterator<Item = (usize, &SalsaField<'s>)> {
self.fields
.iter()
.enumerate()
.filter(|(_, f)| !f.has_tracked_attr)
}
}
impl<'s> SalsaField<'s> {
@ -318,7 +366,7 @@ impl<'s> SalsaField<'s> {
let set_name = Ident::new(&format!("set_{}", field_name_str), field_name.span());
let mut result = SalsaField {
field,
has_id_attr: false,
has_tracked_attr: false,
has_ref_attr: false,
has_default_attr: false,
has_no_eq_attr: false,
@ -337,4 +385,26 @@ impl<'s> SalsaField<'s> {
Ok(result)
}
fn options(&self) -> TokenStream {
let clone_ident = if self.has_ref_attr {
syn::Ident::new("no_clone", Span::call_site())
} else {
syn::Ident::new("clone", Span::call_site())
};
let backdate_ident = if self.has_no_eq_attr {
syn::Ident::new("no_backdate", Span::call_site())
} else {
syn::Ident::new("backdate", Span::call_site())
};
let default_ident = if self.has_default_attr {
syn::Ident::new("default", Span::call_site())
} else {
syn::Ident::new("required", Span::call_site())
};
quote!((#clone_ident, #backdate_ident, #default_ident))
}
}

View file

@ -59,7 +59,7 @@ impl crate::options::AllowedOptions for TrackedStruct {
impl SalsaStructAllowedOptions for TrackedStruct {
const KIND: &'static str = "tracked";
const ALLOW_ID: bool = true;
const ALLOW_TRACKED: bool = true;
const HAS_LIFETIME: bool = true;
@ -85,13 +85,21 @@ impl Macro {
let db_lt = db_lifetime::db_lifetime(&self.struct_item.generics);
let new_fn = salsa_struct.constructor_name();
let field_ids = salsa_struct.field_ids();
let field_vis = salsa_struct.field_vis();
let field_getter_ids = salsa_struct.field_getter_ids();
let tracked_ids = salsa_struct.tracked_ids();
let tracked_vis = salsa_struct.tracked_vis();
let untracked_vis = salsa_struct.untracked_vis();
let tracked_getter_ids = salsa_struct.tracked_getter_ids();
let untracked_getter_ids = salsa_struct.untracked_getter_ids();
let field_indices = salsa_struct.field_indices();
let id_field_indices = salsa_struct.id_field_indices();
let tracked_indices = salsa_struct.tracked_indices();
let untracked_indices = salsa_struct.untracked_indices();
let num_fields = salsa_struct.num_fields();
let field_options = salsa_struct.field_options();
let tracked_options = salsa_struct.tracked_options();
let untracked_options = salsa_struct.untracked_options();
let field_tys = salsa_struct.field_tys();
let tracked_tys = salsa_struct.tracked_tys();
let untracked_tys = salsa_struct.untracked_tys();
let generate_debug_impl = salsa_struct.generate_debug_impl();
let zalsa = self.hygiene.ident("zalsa");
@ -112,11 +120,18 @@ impl Macro {
db_lt: #db_lt,
new_fn: #new_fn,
field_ids: [#(#field_ids),*],
field_getters: [#(#field_vis #field_getter_ids),*],
tracked_ids: [#(#tracked_ids),*],
tracked_getters: [#(#tracked_vis #tracked_getter_ids),*],
untracked_getters: [#(#untracked_vis #untracked_getter_ids),*],
field_tys: [#(#field_tys),*],
tracked_tys: [#(#tracked_tys),*],
untracked_tys: [#(#untracked_tys),*],
field_indices: [#(#field_indices),*],
id_field_indices: [#(#id_field_indices),*],
tracked_indices: [#(#tracked_indices),*],
untracked_indices: [#(#untracked_indices),*],
field_options: [#(#field_options),*],
tracked_options: [#(#tracked_options),*],
untracked_options: [#(#untracked_options),*],
num_fields: #num_fields,
generate_debug_impl: #generate_debug_impl,
unused_names: [

View file

@ -28,6 +28,7 @@ pub struct FunctionId<'db> {
// ANCHOR: program
#[salsa::tracked]
pub struct Program<'db> {
#[tracked]
#[return_ref]
pub statements: Vec<Statement<'db>>,
}
@ -76,14 +77,16 @@ pub enum Op {
// ANCHOR: functions
#[salsa::tracked]
pub struct Function<'db> {
#[id]
pub name: FunctionId<'db>,
#[tracked]
name_span: Span<'db>,
#[tracked]
#[return_ref]
pub args: Vec<VariableId<'db>>,
#[tracked]
#[return_ref]
pub body: Expression<'db>,
}
@ -91,7 +94,9 @@ pub struct Function<'db> {
#[salsa::tracked]
pub struct Span<'db> {
#[tracked]
pub start: usize,
#[tracked]
pub end: usize,
}

View file

@ -26,6 +26,7 @@ pub mod tracked_field;
pub trait Configuration: Sized + 'static {
const DEBUG_NAME: &'static str;
const FIELD_DEBUG_NAMES: &'static [&'static str];
const TRACKED_FIELD_DEBUG_NAMES: &'static [&'static str];
/// A (possibly empty) tuple of the fields for this struct.
type Fields<'db>: Send + Sync;
@ -49,7 +50,7 @@ pub trait Configuration: Sized + 'static {
/// Deref the struct to yield the underlying id.
fn deref_struct(s: Self::Struct<'_>) -> Id;
fn id_fields(fields: &Self::Fields<'_>) -> impl Hash;
fn untracked_fields(fields: &Self::Fields<'_>) -> impl Hash;
/// Create a new value revision array where each element is set to `current_revision`.
fn new_revisions(current_revision: Revision) -> Self::Revisions;
@ -108,7 +109,7 @@ impl<C: Configuration> Jar for JarImpl<C> {
let struct_ingredient = <IngredientImpl<C>>::new(struct_index);
std::iter::once(Box::new(struct_ingredient) as _)
.chain((0..C::FIELD_DEBUG_NAMES.len()).map(|field_index| {
.chain((0..C::TRACKED_FIELD_DEBUG_NAMES.len()).map(|field_index| {
Box::new(<FieldIngredientImpl<C>>::new(struct_index, field_index)) as _
}))
.collect()
@ -368,7 +369,7 @@ where
let identity_hash = IdentityHash {
ingredient_index: self.ingredient_index,
hash: crate::hash::hash(&C::id_fields(&fields)),
hash: crate::hash::hash(&C::untracked_fields(&fields)),
};
let (current_deps, disambiguator) = zalsa_local.disambiguate(identity_hash);
@ -623,10 +624,11 @@ where
unsafe { self.to_self_ref(&value.fields) }
}
/// Access to this value field.
/// Access to this tracked field.
///
/// Note that this function returns the entire tuple of value fields.
/// The caller is responible for selecting the appropriate element.
pub fn field<'db>(
pub fn tracked_field<'db>(
&'db self,
db: &'db dyn crate::Database,
s: C::Struct<'db>,
@ -650,6 +652,25 @@ where
unsafe { self.to_self_ref(&data.fields) }
}
/// Access to this untracked field.
///
/// Note that this function returns the entire tuple of value fields.
/// The caller is responible for selecting the appropriate element.
pub fn untracked_field<'db>(
&'db self,
db: &'db dyn crate::Database,
s: C::Struct<'db>,
_field_index: usize,
) -> &'db C::Fields<'db> {
let (zalsa, _) = db.zalsas();
let id = C::deref_struct(s);
let data = Self::data(zalsa.table(), id);
data.read_lock(zalsa.current_revision());
unsafe { self.to_self_ref(&data.fields) }
}
}
impl<C> Ingredient for IngredientImpl<C>

View file

@ -17,8 +17,8 @@ struct InputWithRecover(u32);
struct InputWithLru(u32);
#[salsa::input]
struct InputWithIdField {
#[id]
struct InputWithTrackedField {
#[tracked]
field: u32,
}

View file

@ -34,15 +34,22 @@ error: `lru` option not allowed here
16 | #[salsa::input(lru =12)]
| ^^^
error: `#[id]` cannot be used with `#[salsa::input]`
error: `#[tracked]` cannot be used with `#[salsa::input]`
--> tests/compile-fail/input_struct_incompatibles.rs:21:5
|
21 | / #[id]
21 | / #[tracked]
22 | | field: u32,
| |______________^
error: cannot find attribute `id` in this scope
error: cannot find attribute `tracked` in this scope
--> tests/compile-fail/input_struct_incompatibles.rs:21:7
|
21 | #[id]
| ^^
21 | #[tracked]
| ^^^^^^^
|
help: consider importing one of these attribute macros
|
1 + use salsa::tracked;
|
1 + use salsa_macros::tracked;
|

View file

@ -29,8 +29,8 @@ struct InternedWithLru {
}
#[salsa::interned]
struct InternedWithIdField {
#[id]
struct InternedWithTrackedField {
#[tracked]
field: u32,
}

View file

@ -34,15 +34,22 @@ error: `lru` option not allowed here
26 | #[salsa::interned(lru = 12)]
| ^^^
error: `#[id]` cannot be used with `#[salsa::interned]`
error: `#[tracked]` cannot be used with `#[salsa::interned]`
--> tests/compile-fail/interned_struct_incompatibles.rs:33:5
|
33 | / #[id]
33 | / #[tracked]
34 | | field: u32,
| |______________^
error: cannot find attribute `id` in this scope
error: cannot find attribute `tracked` in this scope
--> tests/compile-fail/interned_struct_incompatibles.rs:33:7
|
33 | #[id]
| ^^
33 | #[tracked]
| ^^^^^^^
|
help: consider importing one of these attribute macros
|
1 + use salsa::tracked;
|
1 + use salsa_macros::tracked;
|

View file

@ -14,9 +14,9 @@ struct MyInput {
#[salsa::tracked]
struct MyTracked<'db> {
#[id]
identifier: u32,
#[tracked]
#[return_ref]
field: Bomb,
}

View file

@ -28,7 +28,9 @@ fn final_result_depends_on_y(db: &dyn LogDatabase, input: MyInput) -> u32 {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
x: u32,
#[tracked]
y: u32,
}

View file

@ -26,6 +26,7 @@ fn result_depends_on_y(db: &dyn LogDatabase, input: MyInput) -> u32 {
db.push_log(format!("result_depends_on_y({:?})", input));
input.y(db) - 1
}
#[test]
fn execute() {
// result_depends_on_x = x + 1

View file

@ -21,6 +21,7 @@ struct MyInput {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
counter: usize,
}

View file

@ -21,6 +21,7 @@ struct MyInput {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
counter: usize,
}

View file

@ -28,7 +28,6 @@ impl From<bool> for BadEq {
#[salsa::tracked]
struct MyTracked<'db> {
#[id]
field: BadEq,
}

View file

@ -1,9 +1,9 @@
//! Test for a tracked struct where the id field has a
//! Test for a tracked struct where an untracked field has a
//! very poorly chosen hash impl (always returns 0).
//! This demonstrates that the `#[id]` fields on a struct
//! This demonstrates that the `untracked fields on a struct
//! can change values and yet the struct can have the same
//! id (because struct ids are based on the *hash* of the
//! `#[id]` fields).
//! untracked fields).
use salsa::{Database as Db, Setter};
use test_log::test;
@ -32,7 +32,6 @@ impl std::hash::Hash for BadHash {
#[salsa::tracked]
struct MyTracked<'db> {
#[id]
field: BadHash,
}

View file

@ -33,6 +33,7 @@ impl From<bool> for BadEq {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
field: BadEq,
}

View file

@ -23,6 +23,7 @@ impl From<bool> for NotEq {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
#[no_eq]
field: NotEq,
}

View file

@ -32,21 +32,25 @@ struct File {
#[salsa::tracked]
struct Definition<'db> {
#[tracked]
file: File,
}
#[salsa::tracked]
struct Index<'db> {
#[tracked]
definitions: Definitions<'db>,
}
#[salsa::tracked]
struct Definitions<'db> {
#[tracked]
definition: Definition<'db>,
}
#[salsa::tracked]
struct Inference<'db> {
#[tracked]
definition: Definition<'db>,
}

View file

@ -10,6 +10,7 @@ struct MyInput {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
field: MyInterned<'db>,
}

View file

@ -11,7 +11,9 @@ struct MyInput {
#[salsa::tracked]
struct MyTracked<'db> {
#[tracked]
data: MyInput,
#[tracked]
next: MyList<'db>,
}