diff --git a/components/salsa-macro-rules/src/setup_interned_struct.rs b/components/salsa-macro-rules/src/setup_interned_struct.rs index ae4ae515f..e70fa0ea4 100644 --- a/components/salsa-macro-rules/src/setup_interned_struct.rs +++ b/components/salsa-macro-rules/src/setup_interned_struct.rs @@ -315,7 +315,8 @@ macro_rules! setup_interned_struct { )* { let (zalsa, zalsa_local) = db.zalsas(); - $Configuration::ingredient(zalsa).intern(zalsa, zalsa_local, + $zalsa::assert_current_database(db); + $Configuration::ingredient(zalsa).intern(db, zalsa, zalsa_local, StructKey::<$db_lt>($($field_id,)* ::std::marker::PhantomData::default()), |_, data| ($($zalsa::Lookup::into_owned(data.$field_index),)*)) } diff --git a/components/salsa-macro-rules/src/setup_tracked_fn.rs b/components/salsa-macro-rules/src/setup_tracked_fn.rs index 4ce7ccf2e..454f9240c 100644 --- a/components/salsa-macro-rules/src/setup_tracked_fn.rs +++ b/components/salsa-macro-rules/src/setup_tracked_fn.rs @@ -95,23 +95,23 @@ macro_rules! setup_tracked_fn { ) -> ::salsa::plumbing::return_mode_ty!(($return_mode, __), $db_lt, $output_ty) { use ::salsa::plumbing as $zalsa; - $zalsa::attach($db, || { - let (zalsa, zalsa_local) = $db.zalsas(); - let result = $zalsa::macro_if! { - if $needs_interner { - { - let key = $fn_name::intern_ingredient_(zalsa).intern_id(zalsa, zalsa_local, ($($input_id),*), |_, data| data); - $fn_name::fn_ingredient_($db, zalsa).fetch($db, zalsa, zalsa_local, key) - } - } else { - { - $fn_name::fn_ingredient_($db, zalsa).fetch($db, zalsa, zalsa_local, $zalsa::AsId::as_id(&($($input_id),*))) - } + let (zalsa, zalsa_local) = $db.zalsas(); + $zalsa::assert_current_database_or_attached($db, zalsa_local); + + let result = $zalsa::macro_if! { + if $needs_interner { + { + let key = $fn_name::intern_ingredient_(zalsa).intern_id($db, zalsa, zalsa_local, ($($input_id),*), |_, data| data); + $fn_name::fn_ingredient_($db, zalsa).fetch($db, zalsa, zalsa_local, key) } - }; + } else { + { + $fn_name::fn_ingredient_($db, zalsa).fetch($db, zalsa, zalsa_local, $zalsa::AsId::as_id(&($($input_id),*))) + } + } + }; - $zalsa::return_mode_expression!(($return_mode, __), $output_ty, result,) - }) + $zalsa::return_mode_expression!(($return_mode, __), $output_ty, result,) } // The module needs be last in the macro expansion in order to make the tracked @@ -458,10 +458,11 @@ macro_rules! setup_tracked_fn { $($input_id: $interned_input_ty,)* ) -> Vec<&$db_lt A> { use ::salsa::plumbing as $zalsa; + $zalsa::assert_current_database($db); let key = $zalsa::macro_if! { if $needs_interner {{ let (zalsa, zalsa_local) = $db.zalsas(); - $Configuration::intern_ingredient(zalsa).intern_id(zalsa, zalsa_local, ($($input_id),*), |_, data| data) + $Configuration::intern_ingredient(zalsa).intern_id($db, zalsa, zalsa_local, ($($input_id),*), |_, data| data) }} else { $zalsa::AsId::as_id(&($($input_id),*)) } diff --git a/src/attach.rs b/src/attach.rs index 9a9dd0156..5803a26b8 100644 --- a/src/attach.rs +++ b/src/attach.rs @@ -34,6 +34,23 @@ impl Attached { } } + #[inline] + fn is_current_database(&self, db: &Db) -> bool + where + Db: ?Sized + Database, + { + let Some(current_db) = self.database.get() else { + return false; + }; + + let new_db = NonNull::from(db.as_dyn_database()); + if !std::ptr::addr_eq(current_db.as_ptr(), new_db.as_ptr()) { + panic!("Cannot change database mid-query. current: {current_db:?}, new: {new_db:?}"); + } + + true + } + #[inline] fn attach(&self, db: &Db, op: impl FnOnce() -> R) -> R where @@ -49,23 +66,13 @@ impl Attached { impl<'s> DbGuard<'s> { #[inline] fn new(attached: &'s Attached, db: &dyn Database) -> Self { - match attached.database.get() { - // A database is already attached, make sure it's the same as the new one. - Some(current_db) => { - let new_db = NonNull::from(db); - if !std::ptr::addr_eq(current_db.as_ptr(), new_db.as_ptr()) { - panic!( - "Cannot change database mid-query. current: {current_db:?}, new: {new_db:?}" - ); - } - Self { state: None } - } - // No database is attached, attach the new one. - None => { - attached.database.set(Some(NonNull::from(db))); - Self { - state: Some(attached), - } + if attached.is_current_database(db) { + Self { state: None } + } else { + attached.database.set(Some(NonNull::from(db))); + db.zalsa_local().set_attached(true); + Self { + state: Some(attached), } } } @@ -78,7 +85,11 @@ impl Attached { if let Some(attached) = self.state { if let Some(prev) = attached.database.replace(None) { // SAFETY: `prev` is a valid pointer to a database. - unsafe { prev.as_ref().zalsa_local().uncancel() }; + unsafe { + let zalsa_local = prev.as_ref().zalsa_local(); + zalsa_local.set_attached(false); + zalsa_local.uncancel(); + } } } } @@ -119,6 +130,11 @@ impl Attached { } } else { // and it was the a different one from ours, record the state changes. + // SAFETY: Both pointers remain valid for their attachment scopes. + unsafe { + prev.as_ref().zalsa_local().set_attached(false); + db.as_ref().zalsa_local().set_attached(true); + } Self { state: Some(attached), prev: Some(prev), @@ -128,6 +144,8 @@ impl Attached { // No database is attached, attach the new one. None => { attached.database.set(Some(db)); + // SAFETY: `db` remains valid for the attachment scope. + unsafe { db.as_ref().zalsa_local().set_attached(true) }; Self { state: Some(attached), prev: None, @@ -144,7 +162,15 @@ impl Attached { if let Some(attached) = self.state { if let Some(prev) = attached.database.replace(self.prev) { // SAFETY: `prev` is a valid pointer to a database. - unsafe { prev.as_ref().zalsa_local().uncancel() }; + unsafe { + let zalsa_local = prev.as_ref().zalsa_local(); + zalsa_local.set_attached(false); + zalsa_local.uncancel(); + } + } + if let Some(prev) = self.prev { + // SAFETY: `prev` is valid for its enclosing attachment scope. + unsafe { prev.as_ref().zalsa_local().set_attached(true) }; } } } @@ -179,6 +205,87 @@ where ) } +/// Panics if a database other than `db` is currently attached. +#[doc(hidden)] +#[inline] +pub fn assert_current_database(db: &Db) +where + Db: ?Sized + Database, +{ + ATTACHED.with( + #[inline] + |attached| { + attached.is_current_database(db); + }, + ) +} + +/// Panics if a database other than `db` is currently attached, unless `zalsa_local` already +/// records that `db` is attached. +#[doc(hidden)] +#[inline(always)] +pub fn assert_current_database_or_attached( + db: &Db, + zalsa_local: &crate::zalsa_local::ZalsaLocal, +) where + Db: ?Sized + Database, +{ + if !zalsa_local.is_attached() { + assert_current_database_unattached(db); + } +} + +#[cold] +#[inline(never)] +fn assert_current_database_unattached(db: &Db) +where + Db: ?Sized + Database, +{ + assert_current_database(db); +} + +#[inline] +pub(crate) fn attach_if_needed(db: &Db, op: impl FnOnce() -> R) -> R +where + Db: ?Sized + Database, +{ + ATTACHED.with( + #[inline] + |attached| { + if attached.is_current_database(db) { + op() + } else { + attach_cold(attached, db, op) + } + }, + ) +} + +#[cold] +#[inline(never)] +fn attach_cold(attached: &Attached, db: &Db, op: impl FnOnce() -> R) -> R +where + Db: ?Sized + Database, +{ + attached.attach(db, op) +} + +#[inline] +pub(crate) fn is_attached(db: &Db) -> bool +where + Db: ?Sized + Database, +{ + ATTACHED.with( + #[inline] + |attached| { + attached.database.get().is_some_and(|current_db| { + let db = NonNull::from(db.as_dyn_database()); + std::ptr::addr_eq(current_db.as_ptr(), db.as_ptr()) + }) + }, + ) +} + /// Attach the database to the current thread and execute `op`. /// Allows a different database than currently attached. The original database /// will be restored on return. diff --git a/src/database.rs b/src/database.rs index 8a681f693..02fbee25e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -109,8 +109,9 @@ pub trait Database: Send + ZalsaDatabase + AsDynDatabase { /// `salsa_event` is emitted when this method is called, so that should be /// used instead. fn unwind_if_revision_cancelled(&self) { + crate::attach::assert_current_database(self); let (zalsa, zalsa_local) = self.zalsas(); - zalsa.unwind_if_revision_cancelled(zalsa_local); + zalsa.unwind_if_revision_cancelled(self, zalsa_local); } /// Execute `op` with the database in thread-local storage for debug print-outs. diff --git a/src/function/accumulated.rs b/src/function/accumulated.rs index 72f4ccae5..7e75741d4 100644 --- a/src/function/accumulated.rs +++ b/src/function/accumulated.rs @@ -37,6 +37,7 @@ where let mut output = vec![]; // First ensure the result is up to date + crate::attach::assert_current_database(db); self.fetch(db, zalsa, zalsa_local, key); let db_key = self.database_key_index(key); @@ -94,6 +95,7 @@ where ) -> (Option<&'db AccumulatedMap>, InputAccumulatedValues) { let (zalsa, zalsa_local) = db.zalsas(); // NEXT STEP: stash and refactor `fetch` to return an `&Memo` so we can make this work + crate::attach::assert_current_database(db); let memo = self.refresh_memo(db, zalsa, zalsa_local, key); ( memo.header.revisions.accumulated(), diff --git a/src/function/fetch.rs b/src/function/fetch.rs index 8911ea2c9..9e7071efa 100644 --- a/src/function/fetch.rs +++ b/src/function/fetch.rs @@ -1,4 +1,5 @@ use crate::cycle::{CycleRecoveryStrategy, IterationStamp}; +use crate::database::AsDynDatabase; use crate::function::eviction::EvictionPolicy; use crate::function::memo::Memo; use crate::function::sync::ClaimResult; @@ -19,12 +20,17 @@ where zalsa_local: &'db ZalsaLocal, id: Id, ) -> &'db C::Output<'db> { - zalsa.unwind_if_revision_cancelled(zalsa_local); + zalsa.unwind_if_revision_cancelled(db, zalsa_local); let database_key_index = self.database_key_index(id); #[cfg(debug_assertions)] - let _span = crate::tracing::debug_span!("fetch", query = ?database_key_index).entered(); + let _span = crate::tracing::debug_span_with_db!( + db, + "fetch", + query = ?database_key_index + ) + .entered(); let memo = self.refresh_memo(db, zalsa, zalsa_local, id); @@ -35,6 +41,7 @@ where let revisions = &memo.header.revisions; zalsa_local.report_tracked_read( + db, database_key_index, revisions.durability, revisions.changed_at, @@ -62,11 +69,13 @@ where // Keep the hot and cold probes in distinct control-flow blocks. Using `or_else` // here can outline both into one function, making hot hits pay for the cold path's // stack frame. - if let Some(memo) = self.fetch_hot(zalsa, id, memo_ingredient_index) { + if let Some(memo) = self.fetch_hot(db, zalsa, id, memo_ingredient_index) { return memo; } - if let Some(memo) = self.fetch_cold(zalsa, zalsa_local, db, id, memo_ingredient_index) { + if let Some(memo) = crate::attach::attach_if_needed(db, || { + self.fetch_cold(zalsa, zalsa_local, db, id, memo_ingredient_index) + }) { return memo; } } @@ -75,6 +84,7 @@ where #[inline(always)] fn fetch_hot<'db>( &'db self, + db: &'db C::DbView, zalsa: &'db Zalsa, id: Id, memo_ingredient_index: MemoIngredientIndex, @@ -85,13 +95,17 @@ where let database_key_index = self.database_key_index(id); - let can_shallow_update = memo - .header - .shallow_verify_memo(zalsa, database_key_index, true); + let can_shallow_update = + memo.header + .shallow_verify_memo(db.as_dyn_database(), zalsa, database_key_index, true); if can_shallow_update.yes() && !memo.header.may_be_provisional() { - memo.header - .update_shallow(zalsa, database_key_index, can_shallow_update); + memo.header.update_shallow( + db.as_dyn_database(), + zalsa, + database_key_index, + can_shallow_update, + ); // SAFETY: memo is present in memo_map and we have verified that it is // still valid for the current revision. @@ -137,9 +151,13 @@ where if let Some(old_memo) = opt_old_memo { if old_memo.value.is_some() - && old_memo - .header - .verify_memo(db.into(), &claim_guard, C::CYCLE_STRATEGY, true) + && old_memo.header.verify_memo( + db.into(), + db.as_dyn_database(), + &claim_guard, + C::CYCLE_STRATEGY, + true, + ) { // SAFETY: memo is present in memo_map and we have verified that it is // still valid for the current revision. diff --git a/src/function/maybe_changed_after.rs b/src/function/maybe_changed_after.rs index 33df8f3c4..e72200499 100644 --- a/src/function/maybe_changed_after.rs +++ b/src/function/maybe_changed_after.rs @@ -1,6 +1,7 @@ #[cfg(feature = "accumulator")] use crate::accumulator::accumulated_map::InputAccumulatedValues; use crate::cycle::{CycleHeads, CycleRecoveryStrategy, ProvisionalStatus}; +use crate::database::AsDynDatabase; use crate::function::memo::{MemoHeader, TryClaimCycleHeadsIter, TryClaimHeadsResult}; use crate::function::sync::{ClaimGuard, ClaimResult}; use crate::function::{Configuration, IngredientImpl, Reentrancy}; @@ -9,7 +10,7 @@ use std::sync::atomic::Ordering; use crate::key::DatabaseKeyIndex; use crate::zalsa::{MemoIngredientIndex, Zalsa, ZalsaDatabase}; use crate::zalsa_local::{QueryEdgeKind, QueryEdges, QueryOriginRef, QueryRevisions, ZalsaLocal}; -use crate::{Id, Revision}; +use crate::{Database, Id, Revision}; /// Result of memo validation. #[derive(Debug, Copy, Clone)] @@ -92,12 +93,14 @@ where ) -> VerifyResult { let (zalsa, zalsa_local) = db.zalsas(); let memo_ingredient_index = self.memo_ingredient_index(zalsa, id); - zalsa.unwind_if_revision_cancelled(zalsa_local); + crate::attach::assert_current_database(db); + zalsa.unwind_if_revision_cancelled(db, zalsa_local); loop { let database_key_index = self.database_key_index(id); - crate::tracing::debug!( + crate::tracing::debug_with_db!( + db, "{database_key_index:?}: maybe_changed_after(revision = {revision:?})" ); @@ -109,6 +112,7 @@ where }; if let Some(result) = memo.header.maybe_changed_after_hot( + db.as_dyn_database(), zalsa, database_key_index, revision, @@ -117,14 +121,16 @@ where return result; } - if let Some(mcs) = self.maybe_changed_after_cold( - zalsa, - zalsa_local, - db, - id, - revision, - memo_ingredient_index, - ) { + if let Some(mcs) = crate::attach::attach_if_needed(db, || { + self.maybe_changed_after_cold( + zalsa, + zalsa_local, + db, + id, + revision, + memo_ingredient_index, + ) + }) { return mcs; } else { // We failed to claim, have to retry. @@ -170,6 +176,7 @@ where if let Some(result) = old_memo.header.maybe_changed_after_cold( db.into(), + db.as_dyn_database(), &claim_guard, revision, C::CYCLE_STRATEGY, @@ -207,14 +214,15 @@ where impl MemoHeader { fn maybe_changed_after_hot( &self, + db: &dyn Database, zalsa: &Zalsa, database_key_index: DatabaseKeyIndex, revision: Revision, has_value: bool, ) -> Option { - let can_shallow_update = self.shallow_verify_memo(zalsa, database_key_index, has_value); + let can_shallow_update = self.shallow_verify_memo(db, zalsa, database_key_index, has_value); if can_shallow_update.yes() && !self.may_be_provisional() { - self.update_shallow(zalsa, database_key_index, can_shallow_update); + self.update_shallow(db, zalsa, database_key_index, can_shallow_update); Some(if self.revisions.changed_at > revision { VerifyResult::changed() @@ -230,6 +238,7 @@ impl MemoHeader { pub(super) fn verify_memo( &self, db: crate::database::RawDatabase<'_>, + attached_db: &dyn Database, claim_guard: &ClaimGuard<'_>, cycle_recovery_strategy: CycleRecoveryStrategy, has_value: bool, @@ -238,11 +247,12 @@ impl MemoHeader { let zalsa_local = claim_guard.zalsa_local(); let database_key_index = claim_guard.database_key_index(); - let can_shallow_update = self.shallow_verify_memo(zalsa, database_key_index, has_value); + let can_shallow_update = + self.shallow_verify_memo(attached_db, zalsa, database_key_index, has_value); if can_shallow_update.yes() && self.validate_may_be_provisional(zalsa, zalsa_local, database_key_index, has_value) { - self.update_shallow(zalsa, database_key_index, can_shallow_update); + self.update_shallow(attached_db, zalsa, database_key_index, can_shallow_update); true } else { self.deep_verify_memo(db, claim_guard, cycle_recovery_strategy, has_value) @@ -253,6 +263,7 @@ impl MemoHeader { pub(super) fn maybe_changed_after_cold( &self, db: crate::database::RawDatabase<'_>, + attached_db: &dyn Database, claim_guard: &ClaimGuard<'_>, revision: Revision, cycle_recovery_strategy: CycleRecoveryStrategy, @@ -266,7 +277,13 @@ impl MemoHeader { old_memo = self.tracing_debug(has_value) ); - let verified = self.verify_memo(db, claim_guard, cycle_recovery_strategy, has_value); + let verified = self.verify_memo( + db, + attached_db, + claim_guard, + cycle_recovery_strategy, + has_value, + ); if verified { // Check if the inputs are still valid. We can just compare `changed_at`. @@ -290,11 +307,13 @@ impl MemoHeader { #[inline] pub(super) fn shallow_verify_memo( &self, + db: &dyn Database, zalsa: &Zalsa, database_key_index: DatabaseKeyIndex, has_value: bool, ) -> ShallowUpdate { - crate::tracing::debug!( + crate::tracing::debug_with_db!( + db, "{database_key_index:?}: shallow_verify_memo(memo = {memo:#?})", memo = self.tracing_debug(has_value) ); @@ -306,19 +325,21 @@ impl MemoHeader { return ShallowUpdate::Verified; } - self.shallow_verify_memo_cold(zalsa, database_key_index, verified_at) + self.shallow_verify_memo_cold(db, zalsa, database_key_index, verified_at) } #[cold] #[inline(never)] fn shallow_verify_memo_cold( &self, + db: &dyn Database, zalsa: &Zalsa, database_key_index: DatabaseKeyIndex, verified_at: Revision, ) -> ShallowUpdate { let last_changed = zalsa.last_changed_revision(self.revisions.durability); - crate::tracing::trace!( + crate::tracing::trace_with_db!( + db, "{database_key_index:?}: check_durability({database_key_index:#?}, last_changed={:?} <= verified_at={:?}) = {:?}", last_changed, verified_at, @@ -335,13 +356,16 @@ impl MemoHeader { #[inline] pub(super) fn update_shallow( &self, + db: &dyn Database, zalsa: &Zalsa, database_key_index: DatabaseKeyIndex, update: ShallowUpdate, ) { if let ShallowUpdate::HigherDurability = update { - self.mark_as_verified(zalsa, database_key_index); - self.mark_outputs_as_verified(zalsa, database_key_index); + crate::attach::attach_if_needed(db, || { + self.mark_as_verified(zalsa, database_key_index); + self.mark_outputs_as_verified(zalsa, database_key_index); + }); } } diff --git a/src/interned.rs b/src/interned.rs index 1e750d0ef..28f57bab5 100644 --- a/src/interned.rs +++ b/src/interned.rs @@ -211,14 +211,17 @@ impl ValueShared { } /// Record a dependency on this value if its slot can be reused. - fn report_tracked_read_if_reusable( + fn report_tracked_read_if_reusable( + db: &Db, zalsa_local: &ZalsaLocal, index: DatabaseKeyIndex, current_revision: Revision, durability: Durability, - ) { + ) where + Db: ?Sized + crate::Database, + { if Self::is_reusable_with_durability::(durability) { - zalsa_local.report_tracked_read_simple(index, durability, current_revision); + zalsa_local.report_tracked_read_simple_with_db(db, index, durability, current_revision); } else { // The value cannot be reused, so the dependency edge is unnecessary. Its durability // is derived from the active query, but its revision must still contribute to the @@ -346,8 +349,9 @@ where /// /// Note: Using the database within the `assemble` function may result in a deadlock if /// the database ends up trying to intern or allocate a new value. - pub fn intern<'db, Key>( + pub fn intern<'db, Key, Db>( &'db self, + db: &Db, zalsa: &'db Zalsa, zalsa_local: &'db ZalsaLocal, key: Key, @@ -355,9 +359,10 @@ where ) -> C::Struct<'db> where Key: Hash, + Db: ?Sized + crate::Database, C::Fields<'db>: HashEqLike, { - FromId::from_id(self.intern_id(zalsa, zalsa_local, key, assemble)) + FromId::from_id(self.intern_id(db, zalsa, zalsa_local, key, assemble)) } /// Intern data to a unique reference. @@ -370,8 +375,9 @@ where /// /// Note: Using the database within the `assemble` function may result in a deadlock if /// the database ends up trying to intern or allocate a new value. - pub fn intern_id<'db, Key>( + pub fn intern_id<'db, Key, Db>( &'db self, + db: &Db, zalsa: &'db Zalsa, zalsa_local: &'db ZalsaLocal, key: Key, @@ -379,6 +385,7 @@ where ) -> crate::Id where Key: Hash, + Db: ?Sized + crate::Database, // We'd want the following predicate, but this currently implies `'static` due to a rustc // bug // for<'db> C::Data<'db>: HashEqLike, @@ -415,7 +422,7 @@ where if { value_shared.last_interned_at } < current_revision { value_shared.last_interned_at = current_revision; - zalsa.event(&|| { + zalsa.event_with_db(db, &|| { Event::new(EventKind::DidValidateInternedValue { key: index, revision: current_revision, @@ -455,7 +462,8 @@ where // See `intern_id_cold` for why we need to use `current_revision` here. Note that just // because this value was previously interned does not mean it was previously interned // by *our query*, so the same considerations apply. - ValueShared::report_tracked_read_if_reusable::( + ValueShared::report_tracked_read_if_reusable::( + db, zalsa_local, index, current_revision, @@ -468,6 +476,7 @@ where // Fill up the table for the first few revisions without attempting garbage collection. if !self.revision_queue.is_primed() { return self.intern_id_cold( + db, key, zalsa, zalsa_local, @@ -534,7 +543,8 @@ where // Record a dependency on the new value if its slot can be reused. // // See `intern_id_cold` for why we need to use `current_revision` here. - ValueShared::report_tracked_read_if_reusable::( + ValueShared::report_tracked_read_if_reusable::( + db, zalsa_local, index, current_revision, @@ -601,7 +611,7 @@ where drop(old_fields); - zalsa.event(&|| { + zalsa.event_with_db(db, &|| { Event::new(EventKind::DidReuseInternedValue { key: index, revision: current_revision, @@ -612,15 +622,25 @@ where } // If we could not find any stale slots, we are forced to allocate a new one. - self.intern_id_cold(key, zalsa, zalsa_local, assemble, shard, shard_index, hash) + self.intern_id_cold( + db, + key, + zalsa, + zalsa_local, + assemble, + shard, + shard_index, + hash, + ) } /// The cold path for interning a value, allocating a new slot. /// /// Returns `true` if the current thread interned the value. #[allow(clippy::too_many_arguments)] - fn intern_id_cold<'db, Key>( + fn intern_id_cold<'db, Key, Db>( &'db self, + db: &Db, key: Key, zalsa: &Zalsa, zalsa_local: &ZalsaLocal, @@ -631,6 +651,7 @@ where ) -> crate::Id where Key: Hash, + Db: ?Sized + crate::Database, C::Fields<'db>: HashEqLike, { let current_revision = zalsa.current_revision(); @@ -672,14 +693,15 @@ where // the query has changed without a corresponding input changing. Using `current_revision` // for dependencies on interned values encodes the fact that interned IDs are not stable // across revisions. - ValueShared::report_tracked_read_if_reusable::( + ValueShared::report_tracked_read_if_reusable::( + db, zalsa_local, index, current_revision, durability, ); - zalsa.event(&|| { + zalsa.event_with_db(db, &|| { Event::new(EventKind::DidInternValue { key: index, revision: current_revision, diff --git a/src/lib.rs b/src/lib.rs index 2bfbdb8da..0a5308b30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,10 @@ pub mod plumbing { #[cfg(feature = "accumulator")] pub use crate::accumulator::Accumulator; - pub use crate::attach::{attach, with_attached_database}; + pub use crate::attach::{ + assert_current_database, assert_current_database_or_attached, attach, + with_attached_database, + }; pub use crate::cycle::CycleRecoveryStrategy; pub use crate::database::{Database, current_revision}; pub use crate::durability::Durability; diff --git a/src/tracing.rs b/src/tracing.rs index 6d3ae8851..db5815c01 100644 --- a/src/tracing.rs +++ b/src/tracing.rs @@ -25,12 +25,31 @@ macro_rules! debug { }; } +macro_rules! trace_with_db { + ($db:expr, $($x:tt)*) => { + crate::tracing::event_with_db!($db, TRACE, $($x)*) + }; +} + +macro_rules! debug_with_db { + ($db:expr, $($x:tt)*) => { + crate::tracing::event_with_db!($db, DEBUG, $($x)*) + }; +} + macro_rules! debug_span { ($($x:tt)*) => { crate::tracing::span!(DEBUG, $($x)*) }; } +#[allow(unused_macros)] +macro_rules! debug_span_with_db { + ($db:expr, $($x:tt)*) => { + crate::tracing::span_with_db!($db, DEBUG, $($x)*) + }; +} + #[expect(unused_macros)] macro_rules! info_span { ($($x:tt)*) => { @@ -50,6 +69,21 @@ macro_rules! event { }}; } +macro_rules! event_with_db { + ($db:expr, $level:ident, $($x:tt)*) => {{ + if ::tracing::enabled!(::tracing::Level::$level) { + let event = { + #[cold] #[inline(never)] || { + crate::attach($db, || { + ::tracing::event!(::tracing::Level::$level, $($x)*) + }) + } + }; + event(); + } + }}; +} + macro_rules! span { ($level:ident, $($x:tt)*) => {{ let span = { @@ -64,5 +98,26 @@ macro_rules! span { }}; } +#[allow(unused_macros)] +macro_rules! span_with_db { + ($db:expr, $level:ident, $($x:tt)*) => {{ + if ::tracing::enabled!(::tracing::Level::$level) { + let span = { + #[cold] #[inline(never)] || { + crate::attach($db, || { + ::tracing::span!(::tracing::Level::$level, $($x)*) + }) + } + }; + span() + } else { + ::tracing::Span::none() + } + }}; +} + #[expect(unused_imports)] -pub(crate) use {debug, debug_span, event, info, info_span, span, trace, warn_event as warn}; +pub(crate) use { + debug, debug_span, debug_span_with_db, debug_with_db, event, event_with_db, info, info_span, + span, span_with_db, trace, trace_with_db, warn_event as warn, +}; diff --git a/src/zalsa.rs b/src/zalsa.rs index 458a70fe3..16a4ea1fa 100644 --- a/src/zalsa.rs +++ b/src/zalsa.rs @@ -294,9 +294,17 @@ impl Zalsa { /// Cancellation will automatically be triggered by salsa on any query /// invocation. #[inline] - pub(crate) fn unwind_if_revision_cancelled(&self, zalsa_local: &ZalsaLocal) { - self.event(&|| crate::Event::new(crate::EventKind::WillCheckCancellation)); + pub(crate) fn unwind_if_revision_cancelled(&self, db: &Db, zalsa_local: &ZalsaLocal) + where + Db: ?Sized + Database, + { + self.event_with_db(db, &|| { + crate::Event::new(crate::EventKind::WillCheckCancellation) + }); if zalsa_local.should_trigger_local_cancellation() { + if !crate::attach::is_attached(db) { + zalsa_local.uncancel(); + } zalsa_local.unwind_cancelled(); } if self.runtime().load_cancellation_flag() { @@ -468,6 +476,22 @@ impl Zalsa { } } + #[inline(always)] + pub(crate) fn event_with_db(&self, db: &Db, event: &dyn Fn() -> crate::Event) + where + Db: ?Sized + Database, + { + if self.event_callback.is_some() { + self.event_with_db_cold(db.as_dyn_database(), event); + } + } + + #[cold] + #[inline(never)] + fn event_with_db_cold(&self, db: &dyn Database, event: &dyn Fn() -> crate::Event) { + crate::attach(db, || self.event_cold(event)); + } + // Avoid inlining, as events are typically only enabled for debugging purposes. #[cold] #[inline(never)] diff --git a/src/zalsa_local.rs b/src/zalsa_local.rs index 13a2c28bc..b1270605f 100644 --- a/src/zalsa_local.rs +++ b/src/zalsa_local.rs @@ -1,5 +1,5 @@ use std::alloc::{Layout, alloc, dealloc, handle_alloc_error}; -use std::cell::{RefCell, UnsafeCell}; +use std::cell::{Cell, RefCell, UnsafeCell}; use std::fmt; use std::fmt::Formatter; use std::marker::PhantomData; @@ -35,6 +35,12 @@ use crate::{Cancelled, Id, Revision}; /// **Note also that all mutations to the database handle (and hence /// to the local-state) must be undone during unwinding.** pub struct ZalsaLocal { + /// Whether this database handle is the one currently attached to its thread. + /// + /// Mirrored from the thread-local attachment state so hot query entry can establish that the + /// passed database is current without consulting thread-local storage. + attached: Cell, + /// Vector of active queries. /// /// Unwinding note: pushes onto this vector must be popped -- even @@ -88,6 +94,7 @@ impl CancellationToken { impl ZalsaLocal { pub(crate) fn new() -> Self { ZalsaLocal { + attached: Cell::new(false), query_stack: RefCell::new(QueryStack::default()), most_recent_pages: UnsafeCell::new(FxHashMap::default()), cancelled: CancellationToken::default(), @@ -195,6 +202,17 @@ impl ZalsaLocal { } } + /// Returns true if this database handle is currently attached to its thread. + #[inline(always)] + pub(crate) fn is_attached(&self) -> bool { + self.attached.get() + } + + #[inline(always)] + pub(crate) fn set_attached(&self, attached: bool) { + self.attached.set(attached); + } + /// Executes a closure within the context of the current active query stacks (mutable). /// /// # Safety @@ -300,16 +318,20 @@ impl ZalsaLocal { /// Register that currently active query reads the given input #[inline(always)] - pub(crate) fn report_tracked_read( + pub(crate) fn report_tracked_read( &self, + db: &Db, input: DatabaseKeyIndex, durability: Durability, changed_at: Revision, cycle_heads: &CycleHeads, #[cfg(feature = "accumulator")] has_accumulated: bool, #[cfg(feature = "accumulator")] accumulated_inputs: &AtomicInputAccumulatedValues, - ) { - crate::tracing::debug!( + ) where + Db: ?Sized + crate::Database, + { + crate::tracing::debug_with_db!( + db, "report_tracked_read(input={:?}, durability={:?}, changed_at={:?})", input, durability, @@ -350,6 +372,37 @@ impl ZalsaLocal { changed_at ); + self.report_tracked_read_simple_impl(input, durability, changed_at); + } + + #[inline(always)] + pub(crate) fn report_tracked_read_simple_with_db( + &self, + db: &Db, + input: DatabaseKeyIndex, + durability: Durability, + changed_at: Revision, + ) where + Db: ?Sized + crate::Database, + { + crate::tracing::debug_with_db!( + db, + "report_tracked_read(input={:?}, durability={:?}, changed_at={:?})", + input, + durability, + changed_at + ); + + self.report_tracked_read_simple_impl(input, durability, changed_at); + } + + #[inline(always)] + fn report_tracked_read_simple_impl( + &self, + input: DatabaseKeyIndex, + durability: Durability, + changed_at: Revision, + ) { // SAFETY: We do not access the query stack reentrantly. unsafe { self.with_query_stack_unchecked_mut(|stack| { @@ -475,6 +528,7 @@ impl ZalsaLocal { // - `most_recent_pages` can't observe broken states as we cannot panic such that we enter an // inconsistent state // - neither can `query_stack` as we require the closures accessing it to be `UnwindSafe` +// - `attached` is updated in lockstep with thread-local attachment state by its guards impl std::panic::RefUnwindSafe for ZalsaLocal {} /// Summarizes "all the inputs that a query used" and "all the outputs it has written to". diff --git a/tests/attach.rs b/tests/attach.rs new file mode 100644 index 000000000..8d7d3d38a --- /dev/null +++ b/tests/attach.rs @@ -0,0 +1,389 @@ +#![cfg(feature = "inventory")] + +use salsa::{Cancelled, Database as _, DatabaseImpl, Storage}; +use std::cell::Cell; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id, Record}; +use tracing::{Event as TracingEvent, Metadata, Subscriber}; + +#[salsa::tracked] +fn tracked_with_args(_db: &dyn salsa::Database, left: u32, right: u32) -> u32 { + left + right +} + +#[salsa::input] +struct TraceInput { + value: u32, +} + +#[salsa::tracked] +fn populate_reusable_interned_arguments(db: &dyn salsa::Database, input: TraceInput) { + input.value(db); + tracked_with_args(db, 1, 2); +} + +#[salsa::interned] +struct InternedValue<'db> { + value: u32, +} + +#[salsa::tracked] +fn cancel_inside_active_query(db: &dyn salsa::Database) -> u32 { + db.cancellation_token().cancel(); + tracked_with_args(db, 1, 2) +} + +thread_local! { + static OTHER_DATABASE: Cell<*const DatabaseImpl> = const { Cell::new(std::ptr::null()) }; +} + +#[salsa::tracked] +fn call_other_database_from_active_query(_db: &dyn salsa::Database) -> u32 { + OTHER_DATABASE.with(|other_database| { + let other_database = other_database.get(); + assert!(!other_database.is_null()); + // SAFETY: Tests set this pointer immediately before the query call and restore it before + // either database is dropped. + tracked_with_args(unsafe { &*other_database }, 1, 2) + }) +} + +#[salsa::tracked] +fn switch_database_and_call_original(db: &dyn salsa::Database) -> u32 { + OTHER_DATABASE.with(|other_database| { + let other_database = other_database.get(); + assert!(!other_database.is_null()); + // SAFETY: See `call_other_database_from_active_query`. + salsa::attach_allow_change(unsafe { &*other_database }, || tracked_with_args(db, 1, 2)) + }) +} + +fn catch_with_other_database( + other_database: &DatabaseImpl, + op: impl FnOnce() -> R, +) -> std::thread::Result { + OTHER_DATABASE.with(|slot| slot.set(other_database)); + let result = catch_unwind(AssertUnwindSafe(op)); + OTHER_DATABASE.with(|slot| slot.set(std::ptr::null())); + result +} + +fn assert_panics_with(result: std::thread::Result, expected: &str) { + let Err(payload) = result else { + panic!("expected panic containing {expected:?}"); + }; + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or("non-string panic payload"); + assert!(message.contains(expected), "unexpected panic: {message}"); +} + +#[salsa::db] +struct EventDatabase { + storage: Storage, +} + +impl EventDatabase { + fn new(callback: impl Fn(salsa::Event) + Send + Sync + 'static) -> Self { + Self { + storage: Storage::new(Some(Box::new(callback))), + } + } +} + +#[salsa::db] +impl salsa::Database for EventDatabase {} + +#[test] +#[should_panic(expected = "Cannot change database mid-query")] +fn different_database_panics_on_cold_query() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + + db1.attach(|_| tracked_with_args(&db2, 1, 2)); +} + +#[test] +#[should_panic(expected = "Cannot change database mid-query")] +fn different_database_panics_on_hot_query() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + tracked_with_args(&db2, 1, 2); + + db1.attach(|_| tracked_with_args(&db2, 1, 2)); +} + +#[test] +fn different_database_panics_on_hot_query_inside_active_query() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + tracked_with_args(&db2, 1, 2); + + let result = catch_with_other_database(&db2, || call_other_database_from_active_query(&db1)); + + assert_panics_with(result, "Cannot change database mid-query"); +} + +#[test] +fn cloned_database_panics_on_hot_query_inside_active_query() { + let db1 = DatabaseImpl::default(); + let db2 = db1.clone(); + tracked_with_args(&db2, 1, 2); + + let result = catch_with_other_database(&db2, || call_other_database_from_active_query(&db1)); + + assert_panics_with(result, "Cannot change database mid-query"); +} + +#[test] +fn switched_database_does_not_match_suspended_active_query() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + tracked_with_args(&db1, 1, 2); + + let result = catch_with_other_database(&db2, || switch_database_and_call_original(&db1)); + + assert_panics_with(result, "Cannot change database mid-query"); +} + +#[test] +fn attach_allow_change_restores_attachment_markers_after_panic() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + + db1.attach(|_| { + assert_eq!( + salsa::attach_allow_change(&db1, || tracked_with_args(&db1, 1, 2)), + 3 + ); + assert_eq!( + salsa::attach_allow_change(&db2, || tracked_with_args(&db2, 1, 2)), + 3 + ); + assert!( + catch_unwind(AssertUnwindSafe(|| salsa::attach_allow_change( + &db2, + || panic!("switch panic") + ))) + .is_err() + ); + + assert_eq!(tracked_with_args(&db1, 1, 2), 3); + let result = catch_unwind(AssertUnwindSafe(|| tracked_with_args(&db2, 1, 2))); + assert_panics_with(result, "Cannot change database mid-query"); + }); + + let result = catch_unwind(AssertUnwindSafe(|| { + db2.attach(|_| tracked_with_args(&db1, 1, 2)) + })); + assert_panics_with(result, "Cannot change database mid-query"); +} + +#[test] +fn mismatch_panic_restores_attachment() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + tracked_with_args(&db2, 1, 2); + + let result = catch_unwind(AssertUnwindSafe(|| { + db1.attach(|_| tracked_with_args(&db2, 1, 2)) + })); + + assert!(result.is_err()); + assert!(salsa::with_attached_database(|_| ()).is_none()); + assert_eq!(tracked_with_args(&db1, 1, 2), 3); + assert_eq!(tracked_with_args(&db2, 1, 2), 3); +} + +#[test] +fn different_database_panics_before_direct_interning() { + let db1 = DatabaseImpl::default(); + let db2 = DatabaseImpl::default(); + + let result = catch_unwind(AssertUnwindSafe(|| { + db1.attach(|_| InternedValue::new(&db2, 1)) + })); + + assert!(result.is_err()); + assert_eq!(InternedValue::new(&db2, 1).value(&db2), 1); +} + +#[test] +fn top_level_hot_cancellation_is_cleared() { + let db = DatabaseImpl::default(); + assert_eq!(tracked_with_args(&db, 1, 2), 3); + let token = db.cancellation_token(); + token.cancel(); + + let result = Cancelled::catch(|| tracked_with_args(&db, 1, 2)); + + assert!(matches!(result, Err(Cancelled::Local)), "{result:?}"); + assert!(!token.is_cancelled()); + assert!(salsa::with_attached_database(|_| ()).is_none()); + assert_eq!(tracked_with_args(&db, 1, 2), 3); +} + +#[test] +fn outer_attachment_owns_cancellation_cleanup() { + let db = DatabaseImpl::default(); + assert_eq!(tracked_with_args(&db, 1, 2), 3); + let token = db.cancellation_token(); + + db.attach(|_| { + token.cancel(); + let result = Cancelled::catch(|| tracked_with_args(&db, 1, 2)); + assert!(matches!(result, Err(Cancelled::Local)), "{result:?}"); + assert!(token.is_cancelled()); + }); + + assert!(!token.is_cancelled()); + assert_eq!(tracked_with_args(&db, 1, 2), 3); +} + +#[test] +fn active_query_attachment_owns_cancellation_cleanup() { + let db = DatabaseImpl::default(); + assert_eq!(tracked_with_args(&db, 1, 2), 3); + let token = db.cancellation_token(); + + let result = Cancelled::catch(|| cancel_inside_active_query(&db)); + + assert!(matches!(result, Err(Cancelled::Local)), "{result:?}"); + assert!(!token.is_cancelled()); + assert_eq!(tracked_with_args(&db, 1, 2), 3); +} + +#[test] +fn event_callback_for_hot_query_has_attached_database() { + let saw_event = Arc::new(AtomicBool::new(false)); + let db = EventDatabase::new({ + let saw_event = saw_event.clone(); + move |event| { + if matches!(event.kind, salsa::EventKind::WillCheckCancellation) { + assert!(salsa::with_attached_database(|_| ()).is_some()); + saw_event.store(true, Ordering::Relaxed); + } + } + }); + assert_eq!(tracked_with_args(&db, 1, 2), 3); + saw_event.store(false, Ordering::Relaxed); + + assert_eq!(tracked_with_args(&db, 1, 2), 3); + + assert!(saw_event.load(Ordering::Relaxed)); + assert!(salsa::with_attached_database(|_| ()).is_none()); +} + +#[test] +fn event_panic_on_hot_query_cleans_up_attachment_and_cancellation() { + let panic_on_event = Arc::new(AtomicBool::new(false)); + let db = EventDatabase::new({ + let panic_on_event = panic_on_event.clone(); + move |event| { + if matches!(event.kind, salsa::EventKind::WillCheckCancellation) + && panic_on_event.load(Ordering::Relaxed) + { + assert!(salsa::with_attached_database(|_| ()).is_some()); + panic!("event callback panic"); + } + } + }); + assert_eq!(tracked_with_args(&db, 1, 2), 3); + let token = db.cancellation_token(); + token.cancel(); + panic_on_event.store(true, Ordering::Relaxed); + + let result = catch_unwind(AssertUnwindSafe(|| tracked_with_args(&db, 1, 2))); + + assert!(result.is_err()); + assert!(!token.is_cancelled()); + assert!(salsa::with_attached_database(|_| ()).is_none()); + panic_on_event.store(false, Ordering::Relaxed); + assert_eq!(tracked_with_args(&db, 1, 2), 3); +} + +#[derive(Default)] +struct TraceState { + next_span: AtomicU64, + saw_attached_key: AtomicBool, + saw_interned_arguments: AtomicBool, + saw_raw_key: AtomicBool, +} + +struct TraceSubscriber(Arc); + +impl TraceSubscriber { + fn record(&self, record: impl FnOnce(&mut FieldVisitor)) { + let mut visitor = FieldVisitor(&self.0); + record(&mut visitor); + } +} + +impl Subscriber for TraceSubscriber { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, attributes: &Attributes<'_>) -> Id { + self.record(|visitor| attributes.record(visitor)); + Id::from_u64(self.0.next_span.fetch_add(1, Ordering::Relaxed) + 1) + } + + fn record(&self, _span: &Id, values: &Record<'_>) { + self.record(|visitor| values.record(visitor)); + } + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &TracingEvent<'_>) { + self.record(|visitor| event.record(visitor)); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} +} + +struct FieldVisitor<'a>(&'a TraceState); + +impl Visit for FieldVisitor<'_> { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + let value = format!("{}={value:?}", field.name()); + if value.contains("Id(") { + assert!( + salsa::with_attached_database(|_| ()).is_some(), + "database is not attached while recording {value}" + ); + self.0.saw_attached_key.store(true, Ordering::Relaxed); + } + if value.contains("tracked_with_args::interned_arguments") { + self.0.saw_interned_arguments.store(true, Ordering::Relaxed); + } + if value.contains("DatabaseKeyIndex(") { + self.0.saw_raw_key.store(true, Ordering::Relaxed); + } + } +} + +#[test] +fn tracing_hot_query_attaches_database() { + let db = DatabaseImpl::default(); + let input = TraceInput::new(&db, 0); + populate_reusable_interned_arguments(&db, input); + + let state = Arc::new(TraceState::default()); + let dispatch = tracing::Dispatch::new(TraceSubscriber(state.clone())); + + tracing::dispatcher::with_default(&dispatch, || { + tracked_with_args(&db, 1, 2); + }); + + assert!(state.saw_attached_key.load(Ordering::Relaxed)); + assert!(state.saw_interned_arguments.load(Ordering::Relaxed)); + assert!(!state.saw_raw_key.load(Ordering::Relaxed)); +} diff --git a/tests/interned-revisions.rs b/tests/interned-revisions.rs index 705e2f2a9..fb081796b 100644 --- a/tests/interned-revisions.rs +++ b/tests/interned-revisions.rs @@ -6,7 +6,9 @@ mod common; use common::LogDatabase; use expect_test::expect; -use salsa::{Database, Durability, HashEqLike, Lookup, Setter}; +use salsa::{Database, Durability, HashEqLike, Lookup, Setter, Storage}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use test_log::test; #[salsa::input] @@ -61,6 +63,29 @@ struct PanickingInterned<'db> { value: BadHash, } +#[salsa::db] +struct ReuseEventPanicDatabase { + storage: Storage, +} + +impl ReuseEventPanicDatabase { + fn new(panic_on_reuse: Arc) -> Self { + Self { + storage: Storage::new(Some(Box::new(move |event| { + if matches!(event.kind, salsa::EventKind::DidReuseInternedValue { .. }) + && panic_on_reuse.swap(false, Ordering::Relaxed) + { + assert!(salsa::with_attached_database(|_| ()).is_some()); + panic!("reuse event panic"); + } + }))), + } + } +} + +#[salsa::db] +impl Database for ReuseEventPanicDatabase {} + #[salsa::tracked] fn intern_panicking(db: &dyn Database, input: Input) -> PanickingInterned<'_> { PanickingInterned::new(db, PanickingLookup(input.field1(db))) @@ -89,6 +114,34 @@ fn panic_during_reuse_does_not_orphan_slot() { assert_eq!(recovered_id, second_id.next_generation().unwrap()); } +#[test] +fn panic_in_reuse_event_keeps_replacement_reachable() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + #[salsa::tracked] + fn function(db: &dyn Database, input: Input) -> Interned<'_> { + Interned::new(db, BadHash(input.field1(db))) + } + + let panic_on_reuse = Arc::new(AtomicBool::new(false)); + let mut db = ReuseEventPanicDatabase::new(panic_on_reuse.clone()); + let input = Input::new(&db, 0); + + assert_eq!(function(&db, input).field1(&db).0, 0); + input.set_field1(&mut db).to(1); + assert_eq!(function(&db, input).field1(&db).0, 1); + input.set_field1(&mut db).to(2); + assert_eq!(function(&db, input).field1(&db).0, 2); + + input.set_field1(&mut db).to(3); + panic_on_reuse.store(true, Ordering::Relaxed); + let result = catch_unwind(AssertUnwindSafe(|| function(&db, input))); + + assert!(result.is_err()); + assert!(salsa::with_attached_database(|_| ()).is_none()); + assert_eq!(function(&db, input).field1(&db).0, 3); +} + #[salsa::interned] #[derive(Debug)] struct NestedInterned<'db> { diff --git a/tests/interned-structs_self_ref.rs b/tests/interned-structs_self_ref.rs index d843e5962..c6323e9c1 100644 --- a/tests/interned-structs_self_ref.rs +++ b/tests/interned-structs_self_ref.rs @@ -203,6 +203,7 @@ const _: () = { String: zalsa_::HashEqLike, { Configuration_::ingredient(db.zalsa()).intern( + db, db.zalsa(), db.zalsa_local(), StructKey::<'db>(data, std::marker::PhantomData::default()),