diff --git a/components/salsa-2022-macros/src/accumulator.rs b/components/salsa-2022-macros/src/accumulator.rs index 4a6537d4..e3323a00 100644 --- a/components/salsa-2022-macros/src/accumulator.rs +++ b/components/salsa-2022-macros/src/accumulator.rs @@ -25,6 +25,8 @@ impl crate::options::AllowedOptions for Accumulator { const NO_EQ: bool = false; + const SINGLETON: bool = false; + const JAR: bool = true; const DATA: bool = false; @@ -38,6 +40,16 @@ impl crate::options::AllowedOptions for Accumulator { const CONSTRUCTOR_NAME: bool = false; } +impl crate::modes::AllowedModes for Accumulator { + const TRACKED: bool = false; + + const INPUT: bool = false; + + const INTERNED: bool = false; + + const ACCUMULATOR: bool = true; +} + fn accumulator_contents( args: &Args, struct_item: &syn::ItemStruct, diff --git a/components/salsa-2022-macros/src/input.rs b/components/salsa-2022-macros/src/input.rs index e1a815c4..36bbede2 100644 --- a/components/salsa-2022-macros/src/input.rs +++ b/components/salsa-2022-macros/src/input.rs @@ -1,6 +1,6 @@ -use proc_macro2::{Literal, TokenStream}; - +use crate::modes::Mode; use crate::salsa_struct::{SalsaField, SalsaStruct}; +use proc_macro2::{Literal, TokenStream}; /// For an entity struct `Foo` with fields `f1: T1, ..., fN: TN`, we generate... /// @@ -11,16 +11,19 @@ pub(crate) fn input( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - match SalsaStruct::new(args, input).and_then(|el| InputStruct(el).generate_input()) { + let mode = Mode { + ..Default::default() + }; + match SalsaStruct::new(args, input, mode).and_then(|el| InputStruct(el).generate_input()) { Ok(s) => s.into(), Err(err) => err.into_compile_error().into(), } } -struct InputStruct(SalsaStruct); +struct InputStruct(SalsaStruct); impl std::ops::Deref for InputStruct { - type Target = SalsaStruct; + type Target = SalsaStruct; fn deref(&self) -> &Self::Target { &self.0 @@ -33,6 +36,7 @@ impl crate::options::AllowedOptions for InputStruct { const SPECIFY: bool = false; const NO_EQ: bool = false; + const SINGLETON: bool = true; const JAR: bool = true; @@ -47,6 +51,16 @@ impl crate::options::AllowedOptions for InputStruct { const CONSTRUCTOR_NAME: bool = true; } +impl crate::modes::AllowedModes for InputStruct { + const TRACKED: bool = false; + + const INPUT: bool = true; + + const INTERNED: bool = false; + + const ACCUMULATOR: bool = false; +} + impl InputStruct { fn generate_input(&self) -> syn::Result { self.validate_input()?; @@ -73,8 +87,16 @@ impl InputStruct { } fn validate_input(&self) -> syn::Result<()> { + // check for dissalowed fields self.disallow_id_fields("input")?; + // check if an input struct labeled singleton truly has one field + if self.0.is_isingleton() && self.0.num_fields() != 1 { + return Err(syn::Error::new( + self.0.struct_span(), + format!("`Singleton` input mut have only one field"), + )); + } Ok(()) } @@ -127,8 +149,25 @@ impl InputStruct { .collect(); let constructor_name = self.constructor_name(); - parse_quote! { - impl #ident { + let singleton = self.0.is_isingleton(); + + + + let constructor: syn::ImplItemMethod = if singleton { + parse_quote! { + pub fn #constructor_name(__db: &mut #db_dyn_ty, #(#field_names: #field_tys,)*) -> Self + { + let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(__db); + let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient_mut(__jar); + let __id = __ingredients.#input_index.new_singleton_input(__runtime); + #( + __ingredients.#field_indices.store(__runtime, __id, #field_names, salsa::Durability::LOW); + )* + __id + } + } + } else { + parse_quote! { pub fn #constructor_name(__db: &mut #db_dyn_ty, #(#field_names: #field_tys,)*) -> Self { let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(__db); @@ -139,12 +178,54 @@ impl InputStruct { )* __id } + } + }; - #(#field_getters)* + if singleton { + let get: syn::ImplItemMethod = parse_quote! { + #[track_caller] + pub fn get(__db: &#db_dyn_ty) -> Self { + let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db); + let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar); + __ingredients.#input_index.get_singleton_input(__runtime).expect("singleton not yet initialized") + } + }; - #(#field_setters)* + let try_get: syn::ImplItemMethod = parse_quote! { + #[track_caller] + pub fn try_get(__db: &#db_dyn_ty) -> Option { + let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(__db); + let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient(__jar); + __ingredients.#input_index.get_singleton_input(__runtime) + } + }; + + parse_quote! { + impl #ident { + #constructor + + #get + + #try_get + + #(#field_getters)* + + #(#field_setters)* + } + } + } else { + parse_quote! { + impl #ident { + #constructor + + #(#field_getters)* + + #(#field_setters)* + } } } + + // } } /// Generate the `IngredientsFor` impl for this entity. diff --git a/components/salsa-2022-macros/src/interned.rs b/components/salsa-2022-macros/src/interned.rs index f3a4ab85..3a820f0d 100644 --- a/components/salsa-2022-macros/src/interned.rs +++ b/components/salsa-2022-macros/src/interned.rs @@ -1,6 +1,6 @@ -use proc_macro2::TokenStream; - +use crate::modes::Mode; use crate::salsa_struct::SalsaStruct; +use proc_macro2::TokenStream; // #[salsa::interned(jar = Jar0, data = TyData0)] // #[derive(Eq, PartialEq, Hash, Debug, Clone)] @@ -14,16 +14,20 @@ pub(crate) fn interned( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - match SalsaStruct::new(args, input).and_then(|el| InternedStruct(el).generate_interned()) { + let mode = Mode { + ..Default::default() + }; + match SalsaStruct::new(args, input, mode).and_then(|el| InternedStruct(el).generate_interned()) + { Ok(s) => s.into(), Err(err) => err.into_compile_error().into(), } } -struct InternedStruct(SalsaStruct); +struct InternedStruct(SalsaStruct); impl std::ops::Deref for InternedStruct { - type Target = SalsaStruct; + type Target = SalsaStruct; fn deref(&self) -> &Self::Target { &self.0 @@ -37,6 +41,8 @@ impl crate::options::AllowedOptions for InternedStruct { const NO_EQ: bool = false; + const SINGLETON: bool = false; + const JAR: bool = true; const DATA: bool = true; @@ -50,6 +56,16 @@ impl crate::options::AllowedOptions for InternedStruct { const CONSTRUCTOR_NAME: bool = true; } +impl crate::modes::AllowedModes for InternedStruct { + const TRACKED: bool = false; + + const INPUT: bool = true; + + const INTERNED: bool = false; + + const ACCUMULATOR: bool = false; +} + impl InternedStruct { fn generate_interned(&self) -> syn::Result { self.validate_interned()?; diff --git a/components/salsa-2022-macros/src/jar.rs b/components/salsa-2022-macros/src/jar.rs index d4a7f6c2..311b9259 100644 --- a/components/salsa-2022-macros/src/jar.rs +++ b/components/salsa-2022-macros/src/jar.rs @@ -34,6 +34,8 @@ impl crate::options::AllowedOptions for Jar { const NO_EQ: bool = false; + const SINGLETON: bool = false; + const JAR: bool = false; const DATA: bool = false; diff --git a/components/salsa-2022-macros/src/lib.rs b/components/salsa-2022-macros/src/lib.rs index 45b31e67..61388b68 100644 --- a/components/salsa-2022-macros/src/lib.rs +++ b/components/salsa-2022-macros/src/lib.rs @@ -36,6 +36,7 @@ mod db; mod input; mod interned; mod jar; +mod modes; mod options; mod salsa_struct; mod tracked; diff --git a/components/salsa-2022-macros/src/modes.rs b/components/salsa-2022-macros/src/modes.rs new file mode 100644 index 00000000..484f70d4 --- /dev/null +++ b/components/salsa-2022-macros/src/modes.rs @@ -0,0 +1,31 @@ +use std::marker::PhantomData; + +/// The four possible modes of Salsa structs +/// Salsa structs asre generic over AllowedModes. +pub(crate) trait AllowedModes { + const TRACKED: bool; + const INPUT: bool; + const INTERNED: bool; + const ACCUMULATOR: bool; +} + +/// +pub(crate) struct Mode { + pub(super) phantom: PhantomData, +} + +impl Default for Mode { + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + + +impl Mode { + pub(crate) fn singleton_allowed(&self) -> bool { + M::INPUT + } +} + diff --git a/components/salsa-2022-macros/src/options.rs b/components/salsa-2022-macros/src/options.rs index e01245ae..38a8fb7e 100644 --- a/components/salsa-2022-macros/src/options.rs +++ b/components/salsa-2022-macros/src/options.rs @@ -19,6 +19,10 @@ pub(crate) struct Options { /// If this is `Some`, the value is the `no_eq` identifier. pub no_eq: Option, + /// The `singleton` option is used on input with only one field + /// It allows the creation of convenient methods + pub singleton: Option, + /// The `specify` option is used to signal that a tracked function can /// have its value externally specified (at least some of the time). /// @@ -74,6 +78,7 @@ impl Default for Options { constructor_name: Default::default(), phantom: Default::default(), lru: Default::default(), + singleton: Default::default(), } } } @@ -83,6 +88,7 @@ pub(crate) trait AllowedOptions { const RETURN_REF: bool; const SPECIFY: bool; const NO_EQ: bool; + const SINGLETON: bool; const JAR: bool; const DATA: bool; const DB: bool; @@ -141,6 +147,20 @@ impl syn::parse::Parse for Options { "`no_eq` option not allowed here", )); } + } else if ident == "singleton" { + if A::SINGLETON { + if let Some(old) = std::mem::replace(&mut options.singleton, Some(ident)) { + return Err(syn::Error::new( + old.span(), + "option `singleton` provided twice", + )); + } + } else { + return Err(syn::Error::new( + ident.span(), + "`singleton` option not allowed here", + )); + } } else if ident == "specify" { if A::SPECIFY { if let Some(old) = std::mem::replace(&mut options.specify, Some(ident)) { diff --git a/components/salsa-2022-macros/src/salsa_struct.rs b/components/salsa-2022-macros/src/salsa_struct.rs index 420cfcdb..5dd0c984 100644 --- a/components/salsa-2022-macros/src/salsa_struct.rs +++ b/components/salsa-2022-macros/src/salsa_struct.rs @@ -25,41 +25,46 @@ //! * data method `impl Foo { fn data(&self, db: &dyn crate::Db) -> FooData { FooData { f: self.f(db), ... } } }` //! * this could be optimized, particularly for interned fields +use crate::modes::Mode; +use crate::{ + configuration, + modes::AllowedModes, + options::{AllowedOptions, Options}, +}; use heck::ToUpperCamelCase; use proc_macro2::{Ident, Literal, Span, TokenStream}; use syn::spanned::Spanned; -use crate::{ - configuration, - options::{AllowedOptions, Options}, -}; - -pub(crate) struct SalsaStruct { +pub(crate) struct SalsaStruct { args: Options, + _mode: Mode, struct_item: syn::ItemStruct, fields: Vec, } const BANNED_FIELD_NAMES: &[&str] = &["from", "new"]; -impl SalsaStruct { +impl SalsaStruct { pub(crate) fn new( args: proc_macro::TokenStream, input: proc_macro::TokenStream, + mode: Mode, ) -> syn::Result { let struct_item = syn::parse(input)?; - Self::with_struct(args, struct_item) + Self::with_struct(args, struct_item, mode) } pub(crate) fn with_struct( args: proc_macro::TokenStream, struct_item: syn::ItemStruct, + mode: Mode, ) -> syn::Result { - let args = syn::parse(args)?; + let args: Options = syn::parse(args)?; let fields = Self::extract_options(&struct_item)?; - + check_singleton(&mode, args.singleton.as_ref(), struct_item.span())?; Ok(Self { args, + _mode: mode, struct_item, fields, }) @@ -123,6 +128,20 @@ impl SalsaStruct { self.args.jar_ty() } + /// checks if the "singleton" flag was set + pub(crate) fn is_isingleton(&self) -> bool { + self.args.singleton.is_some() + } + + pub(crate) fn num_fields(&self) -> usize { + self.fields.len() + } + + + pub(crate) fn struct_span(&self) -> Span { + self.struct_item.span() + } + pub(crate) fn db_dyn_ty(&self) -> syn::Type { let jar_ty = self.jar_ty(); parse_quote! { @@ -431,3 +450,15 @@ impl SalsaField { !self.has_no_eq_attr } } + + +pub(crate) fn check_singleton(mode: &Mode, sing: Option<&syn::Ident>, s_span: Span) -> syn::Result<()> { + if !mode.singleton_allowed() && sing.is_some() { + Err(syn::Error::new( + s_span, + format!("`Singleton` not allowed for this Salsa struct type"), + )) + } else { + Ok(()) + } +} \ No newline at end of file diff --git a/components/salsa-2022-macros/src/tracked_fn.rs b/components/salsa-2022-macros/src/tracked_fn.rs index 26a818b2..e24ca0ed 100644 --- a/components/salsa-2022-macros/src/tracked_fn.rs +++ b/components/salsa-2022-macros/src/tracked_fn.rs @@ -72,6 +72,8 @@ impl crate::options::AllowedOptions for TrackedFn { const NO_EQ: bool = true; + const SINGLETON: bool = false; + const JAR: bool = true; const DATA: bool = false; diff --git a/components/salsa-2022-macros/src/tracked_struct.rs b/components/salsa-2022-macros/src/tracked_struct.rs index dcc66c74..87f2a156 100644 --- a/components/salsa-2022-macros/src/tracked_struct.rs +++ b/components/salsa-2022-macros/src/tracked_struct.rs @@ -1,6 +1,9 @@ use proc_macro2::{Literal, TokenStream}; -use crate::salsa_struct::{SalsaField, SalsaStruct}; +use crate::{ + modes::Mode, + salsa_struct::{SalsaField, SalsaStruct}, +}; /// For an tracked struct `Foo` with fields `f1: T1, ..., fN: TN`, we generate... /// @@ -11,7 +14,10 @@ pub(crate) fn tracked( args: proc_macro::TokenStream, struct_item: syn::ItemStruct, ) -> proc_macro::TokenStream { - match SalsaStruct::with_struct(args, struct_item) + let mode = Mode { + ..Default::default() + }; + match SalsaStruct::with_struct(args, struct_item, mode) .and_then(|el| TrackedStruct(el).generate_tracked()) { Ok(s) => s.into(), @@ -19,10 +25,10 @@ pub(crate) fn tracked( } } -struct TrackedStruct(SalsaStruct); +struct TrackedStruct(SalsaStruct); impl std::ops::Deref for TrackedStruct { - type Target = SalsaStruct; + type Target = SalsaStruct; fn deref(&self) -> &Self::Target { &self.0 @@ -36,6 +42,8 @@ impl crate::options::AllowedOptions for TrackedStruct { const NO_EQ: bool = false; + const SINGLETON: bool = false; + const JAR: bool = true; const DATA: bool = true; @@ -49,6 +57,16 @@ impl crate::options::AllowedOptions for TrackedStruct { const CONSTRUCTOR_NAME: bool = true; } +impl crate::modes::AllowedModes for TrackedStruct { + const TRACKED: bool = true; + + const INPUT: bool = false; + + const INTERNED: bool = false; + + const ACCUMULATOR: bool = false; +} + impl TrackedStruct { fn generate_tracked(&self) -> syn::Result { self.validate_tracked()?; diff --git a/components/salsa-2022/src/input.rs b/components/salsa-2022/src/input.rs index fa908f6a..029c100a 100644 --- a/components/salsa-2022/src/input.rs +++ b/components/salsa-2022/src/input.rs @@ -46,6 +46,20 @@ where self.counter += 1; Id::from_id(crate::Id::from_u32(next_id)) } + + pub fn new_singleton_input(&mut self, _runtime: &mut Runtime) -> Id { + if self.counter >= 1 { // already exists + Id::from_id(crate::Id::from_u32(self.counter - 1)) + } else { + self.new_input(_runtime) + } + } + + pub fn get_singleton_input(&self, _runtime: &Runtime) -> Option { + (self.counter > 0).then(|| Id::from_id(crate::Id::from_id(crate::Id::from_u32(self.counter - 1)))) + } + + } impl Ingredient for InputIngredient diff --git a/salsa-2022-tests/tests/deletion-cascade.rs b/salsa-2022-tests/tests/deletion-cascade.rs index 9b025a4d..65fc0a6b 100644 --- a/salsa-2022-tests/tests/deletion-cascade.rs +++ b/salsa-2022-tests/tests/deletion-cascade.rs @@ -20,7 +20,7 @@ struct Jar( trait Db: salsa::DbWithJar + HasLogger {} -#[salsa::input] +#[salsa::input(singleton)] struct MyInput { field: u32, } diff --git a/salsa-2022-tests/tests/singleton.rs b/salsa-2022-tests/tests/singleton.rs new file mode 100644 index 00000000..0f40a1c8 --- /dev/null +++ b/salsa-2022-tests/tests/singleton.rs @@ -0,0 +1,53 @@ +//! Basic deletion test: +//! +//! * entities not created in a revision are deleted, as is any memoized data keyed on them. + +use salsa_2022_tests::{HasLogger, Logger}; + +use test_log::test; + +#[salsa::jar(db = Db)] +struct Jar( + MyInput, +); + +trait Db: salsa::DbWithJar + HasLogger {} + +#[salsa::input(singleton)] +struct MyInput { + field: u32, +} + + +#[salsa::db(Jar)] +#[derive(Default)] +struct Database { + storage: salsa::Storage, + logger: Logger, +} + +impl salsa::Database for Database {} + +impl Db for Database {} + +impl HasLogger for Database { + fn logger(&self) -> &Logger { + &self.logger + } +} + +#[test] +fn basic() { + let mut db = Database::default(); + let input1 = MyInput::new(&mut db, 3); + let input2 = MyInput::get(&db); + + assert_eq!(input1, input2); + + let input3 = MyInput::try_get(&db); + assert_eq!(Some(input1), input3); + + let input4 = MyInput::new(&mut db, 3); + + assert_eq!(input2, input4) +}