diff --git a/components/salsa-macro-rules/src/lib.rs b/components/salsa-macro-rules/src/lib.rs index 4834b0f2d..7834b731d 100644 --- a/components/salsa-macro-rules/src/lib.rs +++ b/components/salsa-macro-rules/src/lib.rs @@ -19,6 +19,7 @@ mod maybe_default; mod setup_accumulator_impl; mod setup_input_struct; mod setup_interned_struct; +mod setup_interned_struct_sans_lifetime; mod setup_method_body; mod setup_tracked_fn; mod setup_tracked_struct; diff --git a/components/salsa-macro-rules/src/setup_interned_struct_sans_lifetime.rs b/components/salsa-macro-rules/src/setup_interned_struct_sans_lifetime.rs new file mode 100644 index 000000000..0d0e4ba76 --- /dev/null +++ b/components/salsa-macro-rules/src/setup_interned_struct_sans_lifetime.rs @@ -0,0 +1,213 @@ +/// Macro for setting up a function that must intern its arguments, without any lifetimes. +#[macro_export] +macro_rules! setup_interned_struct_sans_lifetime { + ( + // Attributes on the struct + attrs: [$(#[$attr:meta]),*], + + // Visibility of the struct + vis: $vis:vis, + + // Name of the struct + Struct: $Struct:ident, + + // Name of the struct data. This is a parameter because `std::concat_idents` + // is unstable and taking an addition dependency is unnecessary. + StructData: $StructDataIdent:ident, + + // Name of the `'db` lifetime that the user gave + db_lt: $db_lt:lifetime, + + // the salsa ID + id: $Id:path, + + // Name user gave for `new` + new_fn: $new_fn:ident, + + // A series of option tuples; see `setup_tracked_struct` macro + field_options: [$($field_option:tt),*], + + // Field names + field_ids: [$($field_id:ident),*], + + // Names for field setter methods (typically `set_foo`) + field_getters: [$($field_getter_vis:vis $field_getter_id:ident),*], + + // Field types + field_tys: [$($field_ty:ty),*], + + // Indices for each field from 0..N -- must be unsuffixed (e.g., `0`, `1`). + field_indices: [$($field_index:tt),*], + + // Indexed types for each field (T0, T1, ...) + field_indexed_tys: [$($indexed_ty:ident),*], + + // Number of fields + num_fields: $N:literal, + + // If true, generate a debug impl. + generate_debug_impl: $generate_debug_impl: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, + $zalsa_struct:ident, + $Configuration:ident, + $CACHE:ident, + $Db:ident, + ] + ) => { + $(#[$attr])* + #[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] + $vis struct $Struct( + $Id, + std::marker::PhantomData < &'static salsa::plumbing::interned::Value < $Struct > > + ); + + type $StructDataIdent<$db_lt> = ($($field_ty,)*); + + impl salsa::plumbing::interned::Configuration for $Struct { + const DEBUG_NAME: &'static str = stringify!($Struct); + type Data<'a> = $StructDataIdent<'a>; + type Struct<'a> = $Struct; + fn struct_from_id<'db>(id: salsa::Id) -> Self::Struct<'db> { + use salsa::plumbing::FromId; + $Struct(<$Id>::from_id(id), std::marker::PhantomData) + } + fn deref_struct(s: Self::Struct<'_>) -> salsa::Id { + use salsa::plumbing::AsId; + s.0.as_id() + } + } + + const _: () = { + use salsa::plumbing as $zalsa; + use $zalsa::interned as $zalsa_struct; + + type $Configuration = $Struct; + + /// Key to use during hash lookups. Each field is some type that implements `Lookup` + /// for the owned type. This permits interning with an `&str` when a `String` is required and so forth. + struct StructKey<$db_lt, $($indexed_ty: $zalsa::interned::Lookup<$field_ty>),*>( + $($indexed_ty,)* + std::marker::PhantomData<&$db_lt ()>, + ); + + impl<$db_lt, $($indexed_ty: $zalsa::interned::Lookup<$field_ty>),*> $zalsa::interned::Lookup<$StructDataIdent<$db_lt>> + for StructKey<$db_lt, $($indexed_ty),*> { + + fn hash(&self, h: &mut H) { + $($zalsa::interned::Lookup::hash(&self.$field_index, &mut *h);)* + } + + fn eq(&self, data: &$StructDataIdent<$db_lt>) -> bool { + ($($zalsa::interned::Lookup::eq(&self.$field_index, &data.$field_index) && )* true) + } + + #[allow(unused_unit)] + fn into_owned(self) -> $StructDataIdent<$db_lt> { + ($($zalsa::interned::Lookup::into_owned(self.$field_index),)*) + } + } + + impl $Configuration { + pub fn ingredient(db: &Db) -> &$zalsa_struct::IngredientImpl + where + Db: ?Sized + $zalsa::Database, + { + static CACHE: $zalsa::IngredientCache<$zalsa_struct::IngredientImpl<$Configuration>> = + $zalsa::IngredientCache::new(); + CACHE.get_or_create(db.as_dyn_database(), || { + db.zalsa().add_or_lookup_jar_by_type(&<$zalsa_struct::JarImpl<$Configuration>>::default()) + }) + } + } + + impl $zalsa::AsId for $Struct { + fn as_id(&self) -> salsa::Id { + self.0.as_id() + } + } + + impl $zalsa::FromId for $Struct { + fn from_id(id: salsa::Id) -> Self { + Self(<$Id>::from_id(id), std::marker::PhantomData) + } + } + + unsafe impl Send for $Struct {} + + unsafe impl Sync for $Struct {} + + $zalsa::macro_if! { $generate_debug_impl => + impl std::fmt::Debug for $Struct { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Self::default_debug_fmt(*self, f) + } + } + } + + impl $zalsa::SalsaStructInDb for $Struct { + fn lookup_ingredient_index(aux: &dyn $zalsa::JarAux) -> core::option::Option<$zalsa::IngredientIndex> { + aux.lookup_jar_by_type(&<$zalsa_struct::JarImpl<$Configuration>>::default()) + } + } + + unsafe impl $zalsa::Update for $Struct { + unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { + if unsafe { *old_pointer } != new_value { + unsafe { *old_pointer = new_value }; + true + } else { + false + } + } + } + + impl<$db_lt> $Struct { + pub fn $new_fn<$Db>(db: &$db_lt $Db, $($field_id: impl $zalsa::interned::Lookup<$field_ty>),*) -> Self + where + // FIXME(rust-lang/rust#65991): The `db` argument *should* have the type `dyn Database` + $Db: ?Sized + salsa::Database, + { + let current_revision = $zalsa::current_revision(db); + $Configuration::ingredient(db).intern(db.as_dyn_database(), + StructKey::<$db_lt>($($field_id,)* std::marker::PhantomData::default())) + } + + $( + $field_getter_vis fn $field_getter_id<$Db>(self, db: &'db $Db) -> $zalsa::maybe_cloned_ty!($field_option, 'db, $field_ty) + where + // FIXME(rust-lang/rust#65991): The `db` argument *should* have the type `dyn Database` + $Db: ?Sized + $zalsa::Database, + { + let fields = $Configuration::ingredient(db).fields(db.as_dyn_database(), self); + $zalsa::maybe_clone!( + $field_option, + $field_ty, + &fields.$field_index, + ) + } + )* + + /// Default debug formatting for this struct (may be useful if you define your own `Debug` impl) + pub fn default_debug_fmt(this: Self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + $zalsa::with_attached_database(|db| { + let fields = $Configuration::ingredient(db).fields(db.as_dyn_database(), this); + let mut f = f.debug_struct(stringify!($Struct)); + $( + let f = f.field(stringify!($field_id), &fields.$field_index); + )* + f.finish() + }).unwrap_or_else(|| { + f.debug_tuple(stringify!($Struct)) + .field(&$zalsa::AsId::as_id(&this)) + .finish() + }) + } + } + }; + }; +} diff --git a/components/salsa-macros/src/accumulator.rs b/components/salsa-macros/src/accumulator.rs index 1e09cb08f..f48a1c25e 100644 --- a/components/salsa-macros/src/accumulator.rs +++ b/components/salsa-macros/src/accumulator.rs @@ -42,6 +42,7 @@ impl AllowedOptions for Accumulator { const RECOVERY_FN: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = false; + const ID: bool = true; } struct StructMacro { diff --git a/components/salsa-macros/src/input.rs b/components/salsa-macros/src/input.rs index 9ad444913..3c71b7f1c 100644 --- a/components/salsa-macros/src/input.rs +++ b/components/salsa-macros/src/input.rs @@ -55,6 +55,8 @@ impl crate::options::AllowedOptions for InputStruct { const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; + + const ID: bool = true; } impl SalsaStructAllowedOptions for InputStruct { diff --git a/components/salsa-macros/src/interned.rs b/components/salsa-macros/src/interned.rs index 8caba77e4..92c278a0e 100644 --- a/components/salsa-macros/src/interned.rs +++ b/components/salsa-macros/src/interned.rs @@ -56,6 +56,8 @@ impl crate::options::AllowedOptions for InternedStruct { const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; + + const ID: bool = true; } impl SalsaStructAllowedOptions for InternedStruct { diff --git a/components/salsa-macros/src/interned_sans_lifetime.rs b/components/salsa-macros/src/interned_sans_lifetime.rs new file mode 100644 index 000000000..b2342695d --- /dev/null +++ b/components/salsa-macros/src/interned_sans_lifetime.rs @@ -0,0 +1,138 @@ +use crate::{ + db_lifetime, + hygiene::Hygiene, + options::Options, + salsa_struct::{SalsaStruct, SalsaStructAllowedOptions}, + token_stream_with_error, +}; +use proc_macro2::TokenStream; + +/// For an entity struct `Foo` with fields `f1: T1, ..., fN: TN`, we generate... +/// +/// * the "id struct" `struct Foo(salsa::Id)` +/// * the entity ingredient, which maps the id fields to the `Id` +/// * for each value field, a function ingredient +pub(crate) fn interned_sans_lifetime( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = syn::parse_macro_input!(args as InternedArgs); + let hygiene = Hygiene::from1(&input); + let struct_item = parse_macro_input!(input as syn::ItemStruct); + let m = Macro { + hygiene, + args, + struct_item, + }; + match m.try_macro() { + Ok(v) => v.into(), + Err(e) => token_stream_with_error(input, e), + } +} + +type InternedArgs = Options; + +#[derive(Debug)] +struct InternedStruct; + +impl crate::options::AllowedOptions for InternedStruct { + const RETURN_REF: bool = false; + + const SPECIFY: bool = false; + + const NO_EQ: bool = false; + + const NO_DEBUG: bool = true; + + const NO_CLONE: bool = false; + + const SINGLETON: bool = true; + + const DATA: bool = true; + + const DB: bool = false; + + const RECOVERY_FN: bool = false; + + const LRU: bool = false; + + const CONSTRUCTOR_NAME: bool = true; + + const ID: bool = true; +} + +impl SalsaStructAllowedOptions for InternedStruct { + const KIND: &'static str = "interned"; + + const ALLOW_ID: bool = false; + + const HAS_LIFETIME: bool = false; + + const ALLOW_DEFAULT: bool = false; +} + +struct Macro { + hygiene: Hygiene, + args: InternedArgs, + struct_item: syn::ItemStruct, +} + +impl Macro { + #[allow(non_snake_case)] + fn try_macro(&self) -> syn::Result { + let salsa_struct = SalsaStruct::new(&self.struct_item, &self.args)?; + + let attrs = &self.struct_item.attrs; + let vis = &self.struct_item.vis; + let struct_ident = &self.struct_item.ident; + let struct_data_ident = format_ident!("{}Data", struct_ident); + 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_indices = salsa_struct.field_indices(); + let num_fields = salsa_struct.num_fields(); + let field_vis = salsa_struct.field_vis(); + let field_getter_ids = salsa_struct.field_getter_ids(); + let field_options = salsa_struct.field_options(); + let field_tys = salsa_struct.field_tys(); + let field_indexed_tys = salsa_struct.field_indexed_tys(); + let generate_debug_impl = salsa_struct.generate_debug_impl(); + let id = salsa_struct.id(); + + let zalsa = self.hygiene.ident("zalsa"); + let zalsa_struct = self.hygiene.ident("zalsa_struct"); + let Configuration = self.hygiene.ident("Configuration"); + let CACHE = self.hygiene.ident("CACHE"); + let Db = self.hygiene.ident("Db"); + + Ok(crate::debug::dump_tokens( + struct_ident, + quote! { + salsa::plumbing::setup_interned_struct_sans_lifetime!( + attrs: [#(#attrs),*], + vis: #vis, + Struct: #struct_ident, + StructData: #struct_data_ident, + db_lt: #db_lt, + id: #id, + new_fn: #new_fn, + field_options: [#(#field_options),*], + field_ids: [#(#field_ids),*], + field_getters: [#(#field_vis #field_getter_ids),*], + field_tys: [#(#field_tys),*], + field_indices: [#(#field_indices),*], + field_indexed_tys: [#(#field_indexed_tys),*], + num_fields: #num_fields, + generate_debug_impl: #generate_debug_impl, + unused_names: [ + #zalsa, + #zalsa_struct, + #Configuration, + #CACHE, + #Db, + ] + ); + }, + )) + } +} diff --git a/components/salsa-macros/src/lib.rs b/components/salsa-macros/src/lib.rs index 2b2de5228..688d87355 100644 --- a/components/salsa-macros/src/lib.rs +++ b/components/salsa-macros/src/lib.rs @@ -42,6 +42,7 @@ mod fn_util; mod hygiene; mod input; mod interned; +mod interned_sans_lifetime; mod options; mod salsa_struct; mod tracked; @@ -66,6 +67,55 @@ pub fn interned(args: TokenStream, input: TokenStream) -> TokenStream { interned::interned(args, input) } +/// A discouraged variant of `#[salsa::interned]`. +/// +/// `#[salsa::interned_sans_lifetime]` is intended to be used in codebases that are migrating from +/// the original Salsa to the current version of Salsa. New codebases that are just starting to use +/// Salsa should avoid using this macro and prefer `#[salsa::interned]` instead. +/// +/// `#[salsa::interned_sans_lifetime]` differs from `#[salsa::interned]` in a two key ways: +/// 1. As the name suggests, it removes the `'db` lifetime from the interned struct. This lifetime is +/// designed to meant to certain values as "salsa structs", but it also adds the desirable property +/// of misuse resistance: it is difficult to embed an `#[salsa::interned]` struct into an auxiliary +/// structures or collections collection, which can lead to subtle invalidation bugs. However, old +/// Salsa encouraged storing keys to interned values in auxiliary structures and collections, so +/// so converting all usage to Salsa's current API guidelines might not be desirable or feasible. +/// 2. `#[salsa::interned_sans_lifetime]` requires specifiying the ID. In most cases, `salsa::Id` +/// is sufficent, but in rare, performance-sensitive circumstances, it might be desireable to +/// set the Id to a type that implements `salsa::plumbing::AsId` and `salsa::plumbing::FromId`. +/// +/// ## Example +/// +/// Below is an example of a struct using `#[salsa::interned_sans_lifetime]` with a custom Id: +/// +/// ```no_compile +/// use salsa::plumbing::{AsId, FromId}; +/// +/// #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// struct CustomSalsaIdWrapper(salsa::Id); +/// +/// impl AsId for CustomSalsaIdWrapper { +/// fn as_id(&self) -> salsa::Id { +/// self.0 +/// } +/// } +/// +/// impl FromId for CustomSalsaIdWrapper { +/// fn from_id(id: salsa::Id) -> Self { +/// CustomSalsaIdWrapper(id) +/// } +/// } +/// +/// #[salsa::interned_sans_lifetime(id = CustomSalsaIdWrapper)] +/// struct InternedString { +/// data: String, +/// } +/// ``` +#[proc_macro_attribute] +pub fn interned_sans_lifetime(args: TokenStream, input: TokenStream) -> TokenStream { + interned_sans_lifetime::interned_sans_lifetime(args, input) +} + #[proc_macro_attribute] pub fn input(args: TokenStream, input: TokenStream) -> TokenStream { input::input(args, input) diff --git a/components/salsa-macros/src/options.rs b/components/salsa-macros/src/options.rs index 6f30bb3e6..f16466300 100644 --- a/components/salsa-macros/src/options.rs +++ b/components/salsa-macros/src/options.rs @@ -7,6 +7,7 @@ use syn::{ext::IdentExt, spanned::Spanned}; /// are required and trailing commas are permitted. The options accepted /// for any particular location are configured via the `AllowedOptions` /// trait. +#[derive(Debug)] pub(crate) struct Options { /// The `return_ref` option is used to signal that field/return type is "by ref" /// @@ -66,6 +67,12 @@ pub(crate) struct Options { /// If this is `Some`, the value is the ``. pub constructor_name: Option, + /// The `id = ` option is used to set a custom ID for interrned structs. + /// + /// The custom ID needs to handle + /// If this is `Some`, the value is the ``. + pub id: Option, + /// Remember the `A` parameter, which plays no role after parsing. phantom: PhantomData, } @@ -85,6 +92,7 @@ impl Default for Options { phantom: Default::default(), lru: Default::default(), singleton: Default::default(), + id: Default::default(), } } } @@ -102,6 +110,7 @@ pub(crate) trait AllowedOptions { const RECOVERY_FN: bool; const LRU: bool; const CONSTRUCTOR_NAME: bool; + const ID: bool; } type Equals = syn::Token![=]; @@ -252,7 +261,7 @@ impl syn::parse::Parse for Options { } } else if ident == "constructor" { if A::CONSTRUCTOR_NAME { - let _eq = Equals::parse(input)?; + let _eq: syn::token::Eq = Equals::parse(input)?; let ident = syn::Ident::parse(input)?; if let Some(old) = std::mem::replace(&mut options.constructor_name, Some(ident)) { @@ -267,6 +276,17 @@ impl syn::parse::Parse for Options { "`constructor` option not allowed here", )); } + } else if ident == "id" { + if A::ID { + let _eq = Equals::parse(input)?; + let path = syn::Path::parse(input)?; + options.id = Some(path); + } else { + return Err(syn::Error::new( + ident.span(), + "`id` option not allowed here", + )); + } } else { return Err(syn::Error::new( ident.span(), diff --git a/components/salsa-macros/src/salsa_struct.rs b/components/salsa-macros/src/salsa_struct.rs index 1a24fdc42..6e0759e13 100644 --- a/components/salsa-macros/src/salsa_struct.rs +++ b/components/salsa-macros/src/salsa_struct.rs @@ -118,6 +118,13 @@ where } } + pub(crate) fn id(&self) -> syn::Path { + match &self.args.id { + Some(id) => id.clone(), + None => parse_quote!(salsa::Id), + } + } + /// Disallow `#[id]` attributes on the fields of this struct. /// /// If an `#[id]` field is found, return an error. diff --git a/components/salsa-macros/src/tracked_fn.rs b/components/salsa-macros/src/tracked_fn.rs index 57023ef24..3e9d2f35b 100644 --- a/components/salsa-macros/src/tracked_fn.rs +++ b/components/salsa-macros/src/tracked_fn.rs @@ -44,6 +44,8 @@ impl crate::options::AllowedOptions for TrackedFn { const LRU: bool = true; const CONSTRUCTOR_NAME: bool = false; + + const ID: bool = true; } struct Macro { diff --git a/components/salsa-macros/src/tracked_struct.rs b/components/salsa-macros/src/tracked_struct.rs index 1730b3404..4904d3655 100644 --- a/components/salsa-macros/src/tracked_struct.rs +++ b/components/salsa-macros/src/tracked_struct.rs @@ -50,6 +50,8 @@ impl crate::options::AllowedOptions for TrackedStruct { const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; + + const ID: bool = true; } impl SalsaStructAllowedOptions for TrackedStruct { diff --git a/src/id.rs b/src/id.rs index 859039ac3..06b54b88b 100644 --- a/src/id.rs +++ b/src/id.rs @@ -29,8 +29,9 @@ impl Id { /// In general, you should not need to create salsa ids yourself, /// but it can be useful if you are using the type as a general /// purpose "identifier" internally. + #[doc(hidden)] #[track_caller] - pub(crate) const fn from_u32(x: u32) -> Self { + pub const fn from_u32(x: u32) -> Self { Id { value: match NonZeroU32::new(x + 1) { Some(v) => v, diff --git a/src/lib.rs b/src/lib.rs index 8cc739fab..af28e06b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ pub use salsa_macros::accumulator; pub use salsa_macros::db; pub use salsa_macros::input; pub use salsa_macros::interned; +pub use salsa_macros::interned_sans_lifetime; pub use salsa_macros::tracked; pub use salsa_macros::Update; @@ -111,6 +112,7 @@ pub mod plumbing { pub use salsa_macro_rules::setup_accumulator_impl; pub use salsa_macro_rules::setup_input_struct; pub use salsa_macro_rules::setup_interned_struct; + pub use salsa_macro_rules::setup_interned_struct_sans_lifetime; pub use salsa_macro_rules::setup_method_body; pub use salsa_macro_rules::setup_tracked_fn; pub use salsa_macro_rules::setup_tracked_struct; diff --git a/src/table/memo.rs b/src/table/memo.rs index fe2fc9583..e26293b26 100644 --- a/src/table/memo.rs +++ b/src/table/memo.rs @@ -13,7 +13,7 @@ use crate::{zalsa::MemoIngredientIndex, zalsa_local::QueryOrigin}; /// Every tracked function must take a salsa struct as its first argument /// and memo tables are attached to those salsa structs as auxiliary data. #[derive(Default)] -pub(crate) struct MemoTable { +pub struct MemoTable { memos: RwLock>, } diff --git a/src/table/sync.rs b/src/table/sync.rs index dfe78a23a..58159cfe5 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -18,7 +18,7 @@ use super::util; /// Tracks the keys that are currently being processed; used to coordinate between /// worker threads. #[derive(Default)] -pub(crate) struct SyncTable { +pub struct SyncTable { syncs: RwLock>>, } diff --git a/tests/interned-sans-lifetime.rs b/tests/interned-sans-lifetime.rs new file mode 100644 index 000000000..9f2cbd6f8 --- /dev/null +++ b/tests/interned-sans-lifetime.rs @@ -0,0 +1,160 @@ +use expect_test::expect; +use salsa::plumbing::{AsId, FromId}; +use std::path::{Path, PathBuf}; +use test_log::test; + +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct CustomSalsaIdWrapper(salsa::Id); + +impl AsId for CustomSalsaIdWrapper { + fn as_id(&self) -> salsa::Id { + self.0 + } +} + +impl FromId for CustomSalsaIdWrapper { + fn from_id(id: salsa::Id) -> Self { + CustomSalsaIdWrapper(id) + } +} + +#[salsa::interned_sans_lifetime(id = CustomSalsaIdWrapper)] +struct InternedString { + data: String, +} + +#[salsa::interned_sans_lifetime(id = CustomSalsaIdWrapper)] +struct InternedPair { + data: (InternedString, InternedString), +} + +#[salsa::interned_sans_lifetime(id = CustomSalsaIdWrapper)] +struct InternedTwoFields { + data1: String, + data2: String, +} + +#[salsa::interned_sans_lifetime(id = CustomSalsaIdWrapper)] +struct InternedVec { + data1: Vec, +} + +#[salsa::interned_sans_lifetime] +struct InternedPathBuf { + data1: PathBuf, +} + +#[salsa::tracked] +fn intern_stuff(db: &dyn salsa::Database) -> String { + let s1 = InternedString::new(db, "Hello, "); + let s2 = InternedString::new(db, "World, "); + let s3 = InternedPair::new(db, (s1, s2)); + + format!("{s3:?}") +} + +#[test] +fn execute() { + let db = salsa::DatabaseImpl::new(); + expect![[r#" + "InternedPair { data: (InternedString { data: \"Hello, \" }, InternedString { data: \"World, \" }) }" + "#]].assert_debug_eq(&intern_stuff(&db)); +} + +#[test] +fn interning_returns_equal_keys_for_equal_data() { + let db = salsa::DatabaseImpl::new(); + let s1 = InternedString::new(&db, "Hello, ".to_string()); + let s2 = InternedString::new(&db, "World, ".to_string()); + let s1_2 = InternedString::new(&db, "Hello, "); + let s2_2 = InternedString::new(&db, "World, "); + assert_eq!(s1, s1_2); + assert_eq!(s2, s2_2); +} + +#[test] +fn interning_returns_equal_keys_for_equal_data_multi_field() { + let db = salsa::DatabaseImpl::new(); + let s1 = InternedTwoFields::new(&db, "Hello, ".to_string(), "World"); + let s2 = InternedTwoFields::new(&db, "World, ", "Hello".to_string()); + let s1_2 = InternedTwoFields::new(&db, "Hello, ", "World"); + let s2_2 = InternedTwoFields::new(&db, "World, ", "Hello"); + let new = InternedTwoFields::new(&db, "Hello, World", ""); + + assert_eq!(s1, s1_2); + assert_eq!(s2, s2_2); + assert_ne!(s1, s2_2); + assert_ne!(s1, new); +} + +#[test] +fn interning_vec() { + let db = salsa::DatabaseImpl::new(); + let s1 = InternedVec::new(&db, ["Hello, ".to_string(), "World".to_string()].as_slice()); + let s2 = InternedVec::new(&db, ["Hello, ", "World"].as_slice()); + let s3 = InternedVec::new(&db, vec!["Hello, ".to_string(), "World".to_string()]); + let s4 = InternedVec::new(&db, ["Hello, ", "World"].as_slice()); + let s5 = InternedVec::new(&db, ["Hello, ", "World", "Test"].as_slice()); + let s6 = InternedVec::new(&db, ["Hello, ", "World", ""].as_slice()); + let s7 = InternedVec::new(&db, ["Hello, "].as_slice()); + assert_eq!(s1, s2); + assert_eq!(s1, s3); + assert_eq!(s1, s4); + assert_ne!(s1, s5); + assert_ne!(s1, s6); + assert_ne!(s5, s6); + assert_ne!(s6, s7); +} + +#[test] +fn interning_path_buf() { + let db = salsa::DatabaseImpl::new(); + let s1 = InternedPathBuf::new(&db, PathBuf::from("test_path".to_string())); + let s2 = InternedPathBuf::new(&db, Path::new("test_path")); + let s3 = InternedPathBuf::new(&db, Path::new("test_path/")); + let s4 = InternedPathBuf::new(&db, Path::new("test_path/a")); + assert_eq!(s1, s2); + assert_eq!(s1, s3); + assert_ne!(s1, s4); +} + +#[salsa::tracked] +fn length(db: &dyn salsa::Database, s: InternedString) -> usize { + s.data(db).len() +} + +#[test] +fn tracked_static_query_works() { + let db = salsa::DatabaseImpl::new(); + let s1 = InternedString::new(&db, "Hello, World!".to_string()); + assert_eq!(length(&db, s1), 13); +} + +#[test] +fn public_ingredient() { + let db = salsa::DatabaseImpl::new(); + let s = InternedString::new(&db, String::from("Hello, world!")); + let underlying_id = s.0; + + let data = InternedString::ingredient(&db).data(&db, underlying_id.as_id()); + assert_eq!(data, &(String::from("Hello, world!"),)); +} + +#[salsa::tracked] +fn intern_more_stuff(db: &dyn salsa::Database) -> (InternedString, InternedString, InternedPair) { + let s1 = InternedString::new(db, "Hello, "); + let s2 = InternedString::new(db, "World, "); + let pair = InternedPair::new(db, (s1, s2)); + (s1, s2, pair) +} + +#[test] +fn public_ingredients() { + let db = salsa::DatabaseImpl::new(); + + let (_, _, pair) = intern_more_stuff(&db); + let (interned_s1, interned_s2) = InternedPair::ingredient(&db).fields(&db, pair).0; + + assert_eq!(interned_s1.data(&db), "Hello, ".to_owned()); + assert_eq!(interned_s2.data(&db), "World, ".to_owned()); +}