This commit is contained in:
Niko Matsakis 2024-07-17 08:21:07 -04:00
parent 4e295f2257
commit daba89c278
7 changed files with 150 additions and 450 deletions

View file

@ -21,8 +21,3 @@ mod setup_input_struct;
mod setup_interned_struct; mod setup_interned_struct;
mod setup_tracked_struct; mod setup_tracked_struct;
mod unexpected_cycle_recovery; mod unexpected_cycle_recovery;
#[macro_export]
macro_rules! setup_fn {
() => {};
}

View file

@ -6,4 +6,12 @@ macro_rules! macro_if {
(false => $($t:tt)*) => { (false => $($t:tt)*) => {
}; };
(if true { $($t:tt)* } else { $($f:tt)*}) => {
$($t)*
};
(if false { $($t:tt)* } else { $($f:tt)*}) => {
$($f)*
};
} }

View file

@ -1,315 +1,6 @@
/// Macro for setting up a function with no arguments (but the database).
#[macro_export]
macro_rules! setup_constant_fn {
(
// Attributes on the function
attrs: [$(#[$attr:meta]),*],
// Visibility of the function
vis: $vis:vis,
// Name of the function
fn_name: $fn_name:ident,
// Name of the `'db` lifetime that the user gave; if they didn't, then defaults to `'db`
db_lt: $db_lt:lifetime,
// Path to the database trait that the user's database parameter used
Db: $Db:path,
// Name of the database parameter given by the user.
db: $db:ident,
// Return type of the function (may reference `$generics`).
output_ty: $output_ty:ty,
// Function body, may reference identifiers defined in `$input_pats` and the generics from `$generics`
inner_fn: $inner_fn:item,
// Path to the cycle recovery function to use.
cycle_recovery_fn: ($($cycle_recovery_fn:tt)*),
// Name of cycle recovery strategy variant to use.
cycle_recovery_strategy: $cycle_recovery_strategy:ident,
// Annoyingly macro-rules hygiene does not extend to items defined in the macro.
// We have the procedural macro generate names for those items that are
// not used elsewhere in the user's code.
unused_names: [
$zalsa:ident,
$Configuration:ident,
$FN_CACHE:ident,
$inner:ident,
]
) => {
#[allow(non_camel_case_types)]
$vis struct $fn_name {
_priv: std::convert::Infallible,
}
$(#[$attr])*
$vis fn $fn_name<$db_lt>(
$db: &$db_lt dyn $Db,
) -> $output_ty {
use salsa::plumbing as $zalsa;
struct $Configuration;
static $FN_CACHE: $zalsa::IngredientCache<$zalsa::function::IngredientImpl<$Configuration>> =
$zalsa::IngredientCache::new();
impl $Configuration {
fn fn_ingredient(db: &dyn $Db) -> &$zalsa::function::IngredientImpl<$Configuration> {
$FN_CACHE.get_or_create(db.as_salsa_database(), || {
<dyn $Db as $Db>::zalsa_db(db);
db.add_or_lookup_jar_by_type(&$Configuration)
})
}
}
impl $zalsa::function::Configuration for $Configuration {
const DEBUG_NAME: &'static str = stringify!($fn_name);
type DbView = dyn $Db;
type SalsaStruct<$db_lt> = $zalsa::Singleton;
type Input<$db_lt> = ();
type Output<$db_lt> = $output_ty;
const CYCLE_STRATEGY: $zalsa::CycleRecoveryStrategy = $zalsa::CycleRecoveryStrategy::$cycle_recovery_strategy;
fn should_backdate_value(
old_value: &Self::Output<'_>,
new_value: &Self::Output<'_>,
) -> bool {
$zalsa::should_backdate_value(old_value, new_value)
}
fn execute<'db>($db: &'db Self::DbView, (): ()) -> Self::Output<'db> {
$inner_fn
$inner($db)
}
fn recover_from_cycle<'db>(
db: &$db_lt dyn $Db,
cycle: &$zalsa::Cycle,
(): (),
) -> Self::Output<'db> {
$($cycle_recovery_fn)*(
db,
cycle,
)
}
fn id_to_input<'db>(db: &'db Self::DbView, key: salsa::Id) -> Self::Input<'db> {
()
}
}
impl $zalsa::Jar for $Configuration {
fn create_ingredients(
&self,
first_index: $zalsa::IngredientIndex,
) -> Vec<Box<dyn $zalsa::Ingredient>> {
vec![
Box::new(<$zalsa::function::IngredientImpl<$Configuration>>::new(
first_index,
)),
]
}
}
impl $fn_name {
pub fn accumulated<$db_lt, A: salsa::Accumulator>(
$db: &$db_lt dyn $Db,
) -> Vec<A> {
use salsa::plumbing as $zalsa;
let key = $zalsa::AsId::as_id(&$zalsa::Singleton);
let database_key_index = $Configuration::fn_ingredient($db).database_key_index(key);
$zalsa::accumulated_by($db.as_salsa_database(), database_key_index)
}
}
$zalsa::attach_database($db, || {
$Configuration::fn_ingredient($db).fetch($db, $zalsa::AsId::as_id(&$zalsa::Singleton)).clone()
})
}
};
}
/// Macro for setting up a function that must intern its arguments. /// Macro for setting up a function that must intern its arguments.
#[macro_export] #[macro_export]
macro_rules! setup_struct_fn { macro_rules! setup_fn {
(
// Attributes on the function
attrs: [$(#[$attr:meta]),*],
// Visibility of the function
vis: $vis:vis,
// Name of the function
fn_name: $fn_name:ident,
// Name of the `'db` lifetime that the user gave; if they didn't, then defaults to `'db`
db_lt: $db_lt:lifetime,
// Path to the database trait that the user's database parameter used
Db: $Db:path,
// Name of the database parameter given by the user.
db: $db:ident,
// An identifier for each function argument EXCEPT the database.
// We prefer to use the identifier the user gave, but if the user gave a pattern
// (e.g., `(a, b): (u32, u32)`) we will synthesize an identifier.
input_id: $input_id:ident,
// Types of the function arguments (may reference `$generics`).
input_ty: $input_ty:ty,
// Return type of the function (may reference `$generics`).
output_ty: $output_ty:ty,
// Function body, may reference identifiers defined in `$input_pats` and the generics from `$generics`
inner_fn: $inner_fn:item,
// Path to the cycle recovery function to use.
cycle_recovery_fn: ($($cycle_recovery_fn:tt)*),
// Name of cycle recovery strategy variant to use.
cycle_recovery_strategy: $cycle_recovery_strategy:ident,
// If true, this is specifiable.
is_specifiable: $is_specifiable:tt,
// Annoyingly macro-rules hygiene does not extend to items defined in the macro.
// We have the procedural macro generate names for those items that are
// not used elsewhere in the user's code.
unused_names: [
$zalsa:ident,
$Configuration:ident,
$FN_CACHE:ident,
$inner:ident,
]
) => {
#[allow(non_camel_case_types)]
$vis struct $fn_name {
_priv: std::convert::Infallible,
}
$(#[$attr])*
$vis fn $fn_name<$db_lt>(
$db: &$db_lt dyn $Db,
$input_id: $input_ty,
) -> $output_ty {
use salsa::plumbing as $zalsa;
struct $Configuration;
static $FN_CACHE: $zalsa::IngredientCache<$zalsa::function::IngredientImpl<$Configuration>> =
$zalsa::IngredientCache::new();
impl $Configuration {
fn fn_ingredient(db: &dyn $Db) -> &$zalsa::function::IngredientImpl<$Configuration> {
$FN_CACHE.get_or_create(db.as_salsa_database(), || {
<dyn $Db as $Db>::zalsa_db(db);
db.add_or_lookup_jar_by_type(&$Configuration)
})
}
}
impl $zalsa::function::Configuration for $Configuration {
const DEBUG_NAME: &'static str = stringify!($fn_name);
type DbView = dyn $Db;
type SalsaStruct<$db_lt> = $input_ty;
type Input<$db_lt> = $input_ty;
type Output<$db_lt> = $output_ty;
const CYCLE_STRATEGY: $zalsa::CycleRecoveryStrategy = $zalsa::CycleRecoveryStrategy::$cycle_recovery_strategy;
fn should_backdate_value(
old_value: &Self::Output<'_>,
new_value: &Self::Output<'_>,
) -> bool {
$zalsa::should_backdate_value(old_value, new_value)
}
fn execute<'db>($db: &'db Self::DbView, $input_id: $input_ty) -> Self::Output<'db> {
$inner_fn
$inner($db, $input_id)
}
fn recover_from_cycle<'db>(
db: &$db_lt dyn $Db,
cycle: &$zalsa::Cycle,
$input_id: $input_ty,
) -> Self::Output<'db> {
$($cycle_recovery_fn)*(db, cycle, $input_id)
}
fn id_to_input<'db>(db: &'db Self::DbView, key: salsa::Id) -> Self::Input<'db> {
$zalsa::LookupId::lookup_id(key, db.as_salsa_database())
}
}
impl $zalsa::Jar for $Configuration {
fn create_ingredients(
&self,
first_index: $zalsa::IngredientIndex,
) -> Vec<Box<dyn $zalsa::Ingredient>> {
vec![
Box::new(<$zalsa::function::IngredientImpl<$Configuration>>::new(
first_index,
)),
]
}
}
impl $fn_name {
pub fn accumulated<$db_lt, A: salsa::Accumulator>(
$db: &$db_lt dyn $Db,
$input_id: $input_ty,
) -> Vec<A> {
use salsa::plumbing as $zalsa;
let key = $zalsa::AsId::as_id(&$input_id);
let database_key_index = $Configuration::fn_ingredient($db).database_key_index(key);
$zalsa::accumulated_by($db.as_salsa_database(), database_key_index)
}
$zalsa::macro_if! { $is_specifiable =>
pub fn specify<$db_lt>(
$db: &$db_lt dyn $Db,
$input_id: $input_ty,
value: $output_ty,
) {
let key = $zalsa::AsId::as_id(&$input_id);
$Configuration::fn_ingredient($db).specify_and_record(
$db,
key,
value,
)
}
}
}
$zalsa::attach_database($db, || {
$Configuration::fn_ingredient($db).fetch($db, $zalsa::AsId::as_id(&$input_id)).clone()
})
}
};
}
/// Macro for setting up a function that must intern its arguments.
#[macro_export]
macro_rules! setup_interned_fn {
( (
// Attributes on the function // Attributes on the function
attrs: [$(#[$attr:meta]),*], attrs: [$(#[$attr:meta]),*],
@ -349,6 +40,12 @@ macro_rules! setup_interned_fn {
// Name of cycle recovery strategy variant to use. // Name of cycle recovery strategy variant to use.
cycle_recovery_strategy: $cycle_recovery_strategy:ident, cycle_recovery_strategy: $cycle_recovery_strategy:ident,
// If true, this is specifiable.
is_specifiable: $is_specifiable:tt,
// If true, the input needs an interner (because it has >1 argument).
needs_interner: $needs_interner:tt,
// Annoyingly macro-rules hygiene does not extend to items defined in the macro. // Annoyingly macro-rules hygiene does not extend to items defined in the macro.
// We have the procedural macro generate names for those items that are // We have the procedural macro generate names for those items that are
// not used elsewhere in the user's code. // not used elsewhere in the user's code.
@ -375,17 +72,45 @@ macro_rules! setup_interned_fn {
struct $Configuration; struct $Configuration;
#[derive(Copy, Clone)]
struct $InternedData<$db_lt>(
std::ptr::NonNull<$zalsa::interned::Value<$Configuration>>,
std::marker::PhantomData<&'db $zalsa::interned::Value<$Configuration>>,
);
static $FN_CACHE: $zalsa::IngredientCache<$zalsa::function::IngredientImpl<$Configuration>> = static $FN_CACHE: $zalsa::IngredientCache<$zalsa::function::IngredientImpl<$Configuration>> =
$zalsa::IngredientCache::new(); $zalsa::IngredientCache::new();
static $INTERN_CACHE: $zalsa::IngredientCache<$zalsa::interned::IngredientImpl<$Configuration>> = $zalsa::macro_if! {
$zalsa::IngredientCache::new(); if $needs_interner {
#[derive(Copy, Clone)]
struct $InternedData<$db_lt>(
std::ptr::NonNull<$zalsa::interned::Value<$Configuration>>,
std::marker::PhantomData<&'db $zalsa::interned::Value<$Configuration>>,
);
static $INTERN_CACHE: $zalsa::IngredientCache<$zalsa::interned::IngredientImpl<$Configuration>> =
$zalsa::IngredientCache::new();
impl $zalsa::SalsaStructInDb for $InternedData<'_> {
fn register_dependent_fn(_db: &dyn $zalsa::Database, _index: $zalsa::IngredientIndex) {}
}
impl $zalsa::interned::Configuration for $Configuration {
const DEBUG_NAME: &'static str = "Configuration";
type Data<$db_lt> = ($($input_ty),*);
type Struct<$db_lt> = $InternedData<$db_lt>;
unsafe fn struct_from_raw<'db>(
ptr: std::ptr::NonNull<$zalsa::interned::Value<Self>>,
) -> Self::Struct<'db> {
$InternedData(ptr, std::marker::PhantomData)
}
fn deref_struct(s: Self::Struct<'_>) -> &$zalsa::interned::Value<Self> {
unsafe { s.0.as_ref() }
}
}
} else {
type $InternedData<$db_lt> = ($($input_ty),*);
}
}
impl $Configuration { impl $Configuration {
fn fn_ingredient(db: &dyn $Db) -> &$zalsa::function::IngredientImpl<$Configuration> { fn fn_ingredient(db: &dyn $Db) -> &$zalsa::function::IngredientImpl<$Configuration> {
@ -395,19 +120,17 @@ macro_rules! setup_interned_fn {
}) })
} }
fn intern_ingredient( $zalsa::macro_if! { $needs_interner =>
db: &dyn $Db, fn intern_ingredient(
) -> &$zalsa::interned::IngredientImpl<$Configuration> { db: &dyn $Db,
$INTERN_CACHE.get_or_create(db.as_salsa_database(), || { ) -> &$zalsa::interned::IngredientImpl<$Configuration> {
db.add_or_lookup_jar_by_type(&$Configuration).successor(0) $INTERN_CACHE.get_or_create(db.as_salsa_database(), || {
}) db.add_or_lookup_jar_by_type(&$Configuration).successor(0)
})
}
} }
} }
impl $zalsa::SalsaStructInDb for $InternedData<'_> {
fn register_dependent_fn(_db: &dyn $zalsa::Database, _index: $zalsa::IngredientIndex) {}
}
impl $zalsa::function::Configuration for $Configuration { impl $zalsa::function::Configuration for $Configuration {
const DEBUG_NAME: &'static str = stringify!($fn_name); const DEBUG_NAME: &'static str = stringify!($fn_name);
@ -443,25 +166,13 @@ macro_rules! setup_interned_fn {
} }
fn id_to_input<'db>(db: &'db Self::DbView, key: salsa::Id) -> Self::Input<'db> { fn id_to_input<'db>(db: &'db Self::DbView, key: salsa::Id) -> Self::Input<'db> {
$Configuration::intern_ingredient(db).data(key).clone() $zalsa::macro_if! {
} if $needs_interner {
} $Configuration::intern_ingredient(db).data(key).clone()
} else {
impl $zalsa::interned::Configuration for $Configuration { $zalsa::LookupId::lookup_id(key, db.as_salsa_database())
const DEBUG_NAME: &'static str = "Configuration"; }
}
type Data<$db_lt> = ($($input_ty),*);
type Struct<$db_lt> = $InternedData<$db_lt>;
unsafe fn struct_from_raw<'db>(
ptr: std::ptr::NonNull<$zalsa::interned::Value<Self>>,
) -> Self::Struct<'db> {
$InternedData(ptr, std::marker::PhantomData)
}
fn deref_struct(s: Self::Struct<'_>) -> &$zalsa::interned::Value<Self> {
unsafe { s.0.as_ref() }
} }
} }
@ -470,14 +181,24 @@ macro_rules! setup_interned_fn {
&self, &self,
first_index: $zalsa::IngredientIndex, first_index: $zalsa::IngredientIndex,
) -> Vec<Box<dyn $zalsa::Ingredient>> { ) -> Vec<Box<dyn $zalsa::Ingredient>> {
vec![ $zalsa::macro_if! {
Box::new(<$zalsa::function::IngredientImpl<$Configuration>>::new( if $needs_interner {
first_index, vec![
)), Box::new(<$zalsa::function::IngredientImpl<$Configuration>>::new(
Box::new(<$zalsa::interned::IngredientImpl<$Configuration>>::new( first_index,
first_index.successor(0) )),
)), Box::new(<$zalsa::interned::IngredientImpl<$Configuration>>::new(
] first_index.successor(0)
)),
]
} else {
vec![
Box::new(<$zalsa::function::IngredientImpl<$Configuration>>::new(
first_index,
)),
]
}
}
} }
} }
@ -487,15 +208,42 @@ macro_rules! setup_interned_fn {
$($input_id: $input_ty,)* $($input_id: $input_ty,)*
) -> Vec<A> { ) -> Vec<A> {
use salsa::plumbing as $zalsa; use salsa::plumbing as $zalsa;
let key = $Configuration::intern_ingredient($db).intern_id($db.runtime(), ($($input_id),*)); let key = $zalsa::macro_if! {
if $needs_interner {
$Configuration::intern_ingredient($db).intern_id($db.runtime(), ($($input_id),*))
} else {
$zalsa::AsId::as_id(&($($input_id),*))
}
};
let database_key_index = $Configuration::fn_ingredient($db).database_key_index(key); let database_key_index = $Configuration::fn_ingredient($db).database_key_index(key);
$zalsa::accumulated_by($db.as_salsa_database(), database_key_index) $zalsa::accumulated_by($db.as_salsa_database(), database_key_index)
} }
$zalsa::macro_if! { $is_specifiable =>
pub fn specify<$db_lt>(
$db: &$db_lt dyn $Db,
$($input_id: $input_ty,)*
value: $output_ty,
) {
let key = $zalsa::AsId::as_id(&($($input_id),*));
$Configuration::fn_ingredient($db).specify_and_record(
$db,
key,
value,
)
}
}
} }
$zalsa::attach_database($db, || { $zalsa::attach_database($db, || {
let key = $Configuration::intern_ingredient($db).intern_id($db.runtime(), ($($input_id),*)); $zalsa::macro_if! {
$Configuration::fn_ingredient($db).fetch($db, key).clone() if $needs_interner {
let key = $Configuration::intern_ingredient($db).intern_id($db.runtime(), ($($input_id),*));
$Configuration::fn_ingredient($db).fetch($db, key).clone()
} else {
$Configuration::fn_ingredient($db).fetch($db, $zalsa::AsId::as_id(&($($input_id),*))).clone()
}
}
}) })
} }
}; };

View file

@ -93,78 +93,38 @@ impl Macro {
} }
} }
match function_type { let needs_interner = match function_type {
FunctionType::Constant => Ok(crate::debug::dump_tokens( FunctionType::RequiresInterning => true,
fn_name, FunctionType::Constant | FunctionType::SalsaStruct => false,
quote![salsa::plumbing::setup_constant_fn! { };
attrs: [#(#attrs),*],
vis: #vis, Ok(crate::debug::dump_tokens(
fn_name: #fn_name, fn_name,
db_lt: #db_lt, quote![salsa::plumbing::setup_fn! {
Db: #db_path, attrs: [#(#attrs),*],
db: #db_ident, vis: #vis,
output_ty: #output_ty, fn_name: #fn_name,
inner_fn: #inner_fn, db_lt: #db_lt,
cycle_recovery_fn: #cycle_recovery_fn, Db: #db_path,
cycle_recovery_strategy: #cycle_recovery_strategy, db: #db_ident,
unused_names: [ input_ids: [#(#input_ids),*],
#zalsa, input_tys: [#(#input_tys),*],
#Configuration, output_ty: #output_ty,
#FN_CACHE, inner_fn: #inner_fn,
#inner, cycle_recovery_fn: #cycle_recovery_fn,
] cycle_recovery_strategy: #cycle_recovery_strategy,
}], is_specifiable: #is_specifiable,
)), needs_interner: #needs_interner,
FunctionType::RequiresInterning => Ok(crate::debug::dump_tokens( unused_names: [
fn_name, #zalsa,
quote![salsa::plumbing::setup_interned_fn! { #Configuration,
attrs: [#(#attrs),*], #InternedData,
vis: #vis, #FN_CACHE,
fn_name: #fn_name, #INTERN_CACHE,
db_lt: #db_lt, #inner,
Db: #db_path, ]
db: #db_ident, }],
input_ids: [#(#input_ids),*], ))
input_tys: [#(#input_tys),*],
output_ty: #output_ty,
inner_fn: #inner_fn,
cycle_recovery_fn: #cycle_recovery_fn,
cycle_recovery_strategy: #cycle_recovery_strategy,
unused_names: [
#zalsa,
#Configuration,
#InternedData,
#FN_CACHE,
#INTERN_CACHE,
#inner,
]
}],
)),
FunctionType::SalsaStruct => Ok(crate::debug::dump_tokens(
fn_name,
quote![salsa::plumbing::setup_struct_fn! {
attrs: [#(#attrs),*],
vis: #vis,
fn_name: #fn_name,
db_lt: #db_lt,
Db: #db_path,
db: #db_ident,
input_id: #(#input_ids,)*
input_ty: #(#input_tys,)*
output_ty: #output_ty,
inner_fn: #inner_fn,
cycle_recovery_fn: #cycle_recovery_fn,
cycle_recovery_strategy: #cycle_recovery_strategy,
is_specifiable: #is_specifiable,
unused_names: [
#zalsa,
#Configuration,
#FN_CACHE,
#inner,
]
}],
)),
}
} }
fn validity_check<'item>(&self, item: &'item syn::ItemFn) -> syn::Result<ValidFn<'item>> { fn validity_check<'item>(&self, item: &'item syn::ItemFn) -> syn::Result<ValidFn<'item>> {

View file

@ -114,16 +114,15 @@ impl FromId for Id {
/// As a special case, we permit `Singleton` to be converted to an `Id`. /// As a special case, we permit `Singleton` to be converted to an `Id`.
/// This is useful for declaring functions with no arguments. /// This is useful for declaring functions with no arguments.
impl AsId for salsa_struct::Singleton { impl AsId for () {
fn as_id(&self) -> Id { fn as_id(&self) -> Id {
Id::from_u32(0) Id::from_u32(0)
} }
} }
impl FromId for salsa_struct::Singleton { impl FromId for () {
fn from_id(id: Id) -> Self { fn from_id(id: Id) -> Self {
assert_eq!(0, id.as_u32()); assert_eq!(0, id.as_u32());
salsa_struct::Singleton
} }
} }

View file

@ -79,7 +79,6 @@ pub mod plumbing {
pub use crate::runtime::Stamp; pub use crate::runtime::Stamp;
pub use crate::runtime::StampedValue; pub use crate::runtime::StampedValue;
pub use crate::salsa_struct::SalsaStructInDb; pub use crate::salsa_struct::SalsaStructInDb;
pub use crate::salsa_struct::Singleton;
pub use crate::storage::views; pub use crate::storage::views;
pub use crate::storage::HasStorage; pub use crate::storage::HasStorage;
pub use crate::storage::IngredientCache; pub use crate::storage::IngredientCache;
@ -95,11 +94,9 @@ pub mod plumbing {
pub use salsa_macro_rules::maybe_clone; pub use salsa_macro_rules::maybe_clone;
pub use salsa_macro_rules::maybe_cloned_ty; pub use salsa_macro_rules::maybe_cloned_ty;
pub use salsa_macro_rules::setup_accumulator_impl; pub use salsa_macro_rules::setup_accumulator_impl;
pub use salsa_macro_rules::setup_constant_fn; pub use salsa_macro_rules::setup_fn;
pub use salsa_macro_rules::setup_input_struct; pub use salsa_macro_rules::setup_input_struct;
pub use salsa_macro_rules::setup_interned_fn;
pub use salsa_macro_rules::setup_interned_struct; pub use salsa_macro_rules::setup_interned_struct;
pub use salsa_macro_rules::setup_struct_fn;
pub use salsa_macro_rules::setup_tracked_struct; pub use salsa_macro_rules::setup_tracked_struct;
pub use salsa_macro_rules::unexpected_cycle_recovery; pub use salsa_macro_rules::unexpected_cycle_recovery;

View file

@ -4,13 +4,6 @@ pub trait SalsaStructInDb {
fn register_dependent_fn(db: &dyn Database, index: IngredientIndex); fn register_dependent_fn(db: &dyn Database, index: IngredientIndex);
} }
/// A ZST that implements [`SalsaStructInDb`] impl SalsaStructInDb for () {
///
/// It is used for implementing "constant" tracked function
/// (ones that only take a database as an argument).
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub struct Singleton;
impl SalsaStructInDb for Singleton {
fn register_dependent_fn(_db: &dyn Database, _index: IngredientIndex) {} fn register_dependent_fn(_db: &dyn Database, _index: IngredientIndex) {}
} }