mirror of
https://github.com/salsa-rs/salsa.git
synced 2025-01-22 21:05:11 +00:00
Support singleton inputs
This commit is contained in:
parent
657b85682b
commit
565c53d4dd
13 changed files with 310 additions and 29 deletions
|
@ -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,
|
||||
|
|
|
@ -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<Self>);
|
||||
struct InputStruct(SalsaStruct<Self, Self>);
|
||||
|
||||
impl std::ops::Deref for InputStruct {
|
||||
type Target = SalsaStruct<Self>;
|
||||
type Target = SalsaStruct<Self, Self>;
|
||||
|
||||
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<TokenStream> {
|
||||
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();
|
||||
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! {
|
||||
impl #ident {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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")
|
||||
}
|
||||
};
|
||||
|
||||
let try_get: syn::ImplItemMethod = parse_quote! {
|
||||
#[track_caller]
|
||||
pub fn try_get(__db: &#db_dyn_ty) -> Option<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)
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
|
|
|
@ -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<Self>);
|
||||
struct InternedStruct(SalsaStruct<Self, Self>);
|
||||
|
||||
impl std::ops::Deref for InternedStruct {
|
||||
type Target = SalsaStruct<Self>;
|
||||
type Target = SalsaStruct<Self, Self>;
|
||||
|
||||
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<TokenStream> {
|
||||
self.validate_interned()?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -36,6 +36,7 @@ mod db;
|
|||
mod input;
|
||||
mod interned;
|
||||
mod jar;
|
||||
mod modes;
|
||||
mod options;
|
||||
mod salsa_struct;
|
||||
mod tracked;
|
||||
|
|
31
components/salsa-2022-macros/src/modes.rs
Normal file
31
components/salsa-2022-macros/src/modes.rs
Normal file
|
@ -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<M: AllowedModes> {
|
||||
pub(super) phantom: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M: AllowedModes> Default for Mode<M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<M: AllowedModes> Mode<M> {
|
||||
pub(crate) fn singleton_allowed(&self) -> bool {
|
||||
M::INPUT
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,10 @@ pub(crate) struct Options<A: AllowedOptions> {
|
|||
/// If this is `Some`, the value is the `no_eq` identifier.
|
||||
pub no_eq: Option<syn::Ident>,
|
||||
|
||||
/// The `singleton` option is used on input with only one field
|
||||
/// It allows the creation of convenient methods
|
||||
pub singleton: Option<syn::Ident>,
|
||||
|
||||
/// 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<A: AllowedOptions> Default for Options<A> {
|
|||
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<A: AllowedOptions> syn::parse::Parse for Options<A> {
|
|||
"`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)) {
|
||||
|
|
|
@ -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<A: AllowedOptions> {
|
||||
pub(crate) struct SalsaStruct<A: AllowedOptions, M: AllowedModes> {
|
||||
args: Options<A>,
|
||||
_mode: Mode<M>,
|
||||
struct_item: syn::ItemStruct,
|
||||
fields: Vec<SalsaField>,
|
||||
}
|
||||
|
||||
const BANNED_FIELD_NAMES: &[&str] = &["from", "new"];
|
||||
|
||||
impl<A: AllowedOptions> SalsaStruct<A> {
|
||||
impl<A: AllowedOptions, M: AllowedModes> SalsaStruct<A, M> {
|
||||
pub(crate) fn new(
|
||||
args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
mode: Mode<M>,
|
||||
) -> syn::Result<Self> {
|
||||
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<M>,
|
||||
) -> syn::Result<Self> {
|
||||
let args = syn::parse(args)?;
|
||||
let args: Options<A> = 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<A: AllowedOptions> SalsaStruct<A> {
|
|||
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<M: AllowedModes>(mode: &Mode<M>, 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(())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<Self>);
|
||||
struct TrackedStruct(SalsaStruct<Self, Self>);
|
||||
|
||||
impl std::ops::Deref for TrackedStruct {
|
||||
type Target = SalsaStruct<Self>;
|
||||
type Target = SalsaStruct<Self, Self>;
|
||||
|
||||
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<TokenStream> {
|
||||
self.validate_tracked()?;
|
||||
|
|
|
@ -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<Id> {
|
||||
(self.counter > 0).then(|| Id::from_id(crate::Id::from_id(crate::Id::from_u32(self.counter - 1))))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl<DB: ?Sized, Id> Ingredient<DB> for InputIngredient<Id>
|
||||
|
|
|
@ -20,7 +20,7 @@ struct Jar(
|
|||
|
||||
trait Db: salsa::DbWithJar<Jar> + HasLogger {}
|
||||
|
||||
#[salsa::input]
|
||||
#[salsa::input(singleton)]
|
||||
struct MyInput {
|
||||
field: u32,
|
||||
}
|
||||
|
|
53
salsa-2022-tests/tests/singleton.rs
Normal file
53
salsa-2022-tests/tests/singleton.rs
Normal file
|
@ -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<Jar> + HasLogger {}
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
struct MyInput {
|
||||
field: u32,
|
||||
}
|
||||
|
||||
|
||||
#[salsa::db(Jar)]
|
||||
#[derive(Default)]
|
||||
struct Database {
|
||||
storage: salsa::Storage<Self>,
|
||||
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)
|
||||
}
|
Loading…
Reference in a new issue