Skip to content

feature: support interned structs without lifetimes #618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/salsa-macro-rules/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/// 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 `'db` lifetime that the user gave
db_lt: $db_lt:lifetime,

// 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(
salsa::Id,
std::marker::PhantomData < &'static salsa::plumbing::interned::Value < $Struct > >
);

const _: () = {
use salsa::plumbing as $zalsa;
use $zalsa::interned as $zalsa_struct;

type $Configuration = $Struct;

type StructData<$db_lt> = ($($field_ty,)*);

/// Key to use during hash lookups. Each field is some type that implements `Lookup<T>`
/// 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<StructData<$db_lt>>
for StructKey<$db_lt, $($indexed_ty),*> {

fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
$($zalsa::interned::Lookup::hash(&self.$field_index, &mut *h);)*
}

fn eq(&self, data: &StructData<$db_lt>) -> bool {
($($zalsa::interned::Lookup::eq(&self.$field_index, &data.$field_index) && )* true)
}

#[allow(unused_unit)]
fn into_owned(self) -> StructData<$db_lt> {
($($zalsa::interned::Lookup::into_owned(self.$field_index),)*)
}
}

impl $zalsa_struct::Configuration for $Configuration {
const DEBUG_NAME: &'static str = stringify!($Struct);
type Data<'a> = StructData<'a>;
type Struct<'a> = $Struct;
fn struct_from_id<'db>(id: salsa::Id) -> Self::Struct<'db> {
$Struct(id, std::marker::PhantomData)
}
fn deref_struct(s: Self::Struct<'_>) -> salsa::Id {
s.0
}
}

impl $Configuration {
pub fn ingredient<Db>(db: &Db) -> &$zalsa_struct::IngredientImpl<Self>
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
}
}

impl $zalsa::FromId for $Struct {
fn from_id(id: salsa::Id) -> Self {
Self(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 {
}

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()
})
}
}
};
};
}
131 changes: 131 additions & 0 deletions components/salsa-macros/src/interned_sans_lifetime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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<InternedStruct>;

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;
}

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<TokenStream> {
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 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 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,
db_lt: #db_lt,
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,
]
);
},
))
}
}
6 changes: 6 additions & 0 deletions components/salsa-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod fn_util;
mod hygiene;
mod input;
mod interned;
mod interned_sans_lifetime;
mod options;
mod salsa_struct;
mod tracked;
Expand All @@ -66,6 +67,11 @@ pub fn interned(args: TokenStream, input: TokenStream) -> TokenStream {
interned::interned(args, input)
}

#[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)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading