salsa-macros-0.23.0/.cargo_vcs_info.json0000644000000001650000000000100135050ustar { "git": { "sha1": "572d144b33c766c792239c98b470265aaab3fef0" }, "path_in_vcs": "components/salsa-macros" }salsa-macros-0.23.0/CHANGELOG.md000064400000000000000000000073451046102023000141150ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.23.0](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.22.0...salsa-macros-v0.23.0) - 2025-06-27 ### Added - `Update` derive field overwrite support ([#747](https://github.com/salsa-rs/salsa/pull/747)) ### Other - Emit self ty for query debug name of assoc function queries ([#927](https://github.com/salsa-rs/salsa/pull/927)) - add option to track heap memory usage of memos ([#925](https://github.com/salsa-rs/salsa/pull/925)) - add an option to tune interned garbage collection ([#911](https://github.com/salsa-rs/salsa/pull/911)) - Preserve attributes on interned/tracked struct fields ([#905](https://github.com/salsa-rs/salsa/pull/905)) - Update dependencies, remove unused `heck` dependency ([#894](https://github.com/salsa-rs/salsa/pull/894)) - Allow lifetimes in arguments in tracked fns with >1 parameters ([#880](https://github.com/salsa-rs/salsa/pull/880)) ## [0.22.0](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.21.1...salsa-macros-v0.22.0) - 2025-05-23 ### Other - Allow creation of tracked associated functions (without `self`) ([#859](https://github.com/salsa-rs/salsa/pull/859)) - Implement an `!Update` bound escape hatch for tracked fn ([#867](https://github.com/salsa-rs/salsa/pull/867)) - Fix returns(deref | as_ref | as_deref) in tracked methods ([#857](https://github.com/salsa-rs/salsa/pull/857)) - Changed `return_ref` syntax to `returns(as_ref)` and `returns(cloned)` ([#772](https://github.com/salsa-rs/salsa/pull/772)) - Move salsa event system into `Zalsa` ([#849](https://github.com/salsa-rs/salsa/pull/849)) ## [0.21.0](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.20.0...salsa-macros-v0.21.0) - 2025-04-29 ### Fixed - allow unused lifetimes in tracked_struct expansion ([#824](https://github.com/salsa-rs/salsa/pull/824)) ### Other - Add a compile-fail test for a `'static` `!Update` struct ([#820](https://github.com/salsa-rs/salsa/pull/820)) - squelch most clippy warnings in generated code ([#809](https://github.com/salsa-rs/salsa/pull/809)) - Use `DatabaseKey` for interned events ([#813](https://github.com/salsa-rs/salsa/pull/813)) ## [0.20.0](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.19.0...salsa-macros-v0.20.0) - 2025-04-22 ### Added - Drop `Debug` requirements and flip implementation defaults ([#756](https://github.com/salsa-rs/salsa/pull/756)) ### Other - Add a third cycle mode, equivalent to old Salsa cycle behavior ([#801](https://github.com/salsa-rs/salsa/pull/801)) - Normalize imports style ([#779](https://github.com/salsa-rs/salsa/pull/779)) - Document most safety blocks ([#776](https://github.com/salsa-rs/salsa/pull/776)) - bug [salsa-macros]: Improve debug name of tracked methods ([#755](https://github.com/salsa-rs/salsa/pull/755)) - rewrite cycle handling to support fixed-point iteration ([#603](https://github.com/salsa-rs/salsa/pull/603)) ## [0.19.0](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.18.0...salsa-macros-v0.19.0) - 2025-03-10 ### Fixed - fix enums bug ### Other - Store view downcaster in function ingredients directly ([#720](https://github.com/salsa-rs/salsa/pull/720)) - :replace instead of std::mem::replace ([#746](https://github.com/salsa-rs/salsa/pull/746)) - Cleanup `Cargo.toml`s ([#745](https://github.com/salsa-rs/salsa/pull/745)) - address review comments - Skip memo ingredient index mapping for non enum tracked functions - Trade off a bit of memory for more speed in `MemoIngredientIndices` - Introduce Salsa enums - Track revisions for tracked fields only salsa-macros-0.23.0/Cargo.lock0000644000000025510000000000100114610ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "salsa-macros" version = "0.23.0" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" salsa-macros-0.23.0/Cargo.toml0000644000000021430000000000100115010ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.85" name = "salsa-macros" version = "0.23.0" authors = ["Salsa developers"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Procedural macros for the salsa crate" readme = false license = "Apache-2.0 OR MIT" repository = "https://github.com/salsa-rs/salsa" [lib] name = "salsa_macros" path = "src/lib.rs" proc-macro = true [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.syn] version = "2.0.101" features = [ "full", "visit-mut", ] [dependencies.synstructure] version = "0.13.2" salsa-macros-0.23.0/Cargo.toml.orig000064400000000000000000000006211046102023000151610ustar 00000000000000[package] name = "salsa-macros" version = "0.23.0" authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true description = "Procedural macros for the salsa crate" [lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0.101", features = ["full", "visit-mut"] } synstructure = "0.13.2" salsa-macros-0.23.0/src/accumulator.rs000064400000000000000000000044701046102023000157540ustar 00000000000000use proc_macro2::TokenStream; use crate::hygiene::Hygiene; use crate::options::{AllowedOptions, Options}; use crate::token_stream_with_error; // #[salsa::accumulator(jar = Jar0)] // struct Accumulator(DataType); pub(crate) fn accumulator( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let hygiene = Hygiene::from1(&input); let args = syn::parse_macro_input!(args as Options); let struct_item = parse_macro_input!(input as syn::ItemStruct); let ident = struct_item.ident.clone(); let m = StructMacro { hygiene, _args: args, struct_item, }; match m.try_expand() { Ok(v) => crate::debug::dump_tokens(ident, v).into(), Err(e) => token_stream_with_error(input, e), } } struct Accumulator; impl AllowedOptions for Accumulator { const RETURNS: bool = false; const SPECIFY: bool = false; const NO_EQ: bool = false; const DEBUG: bool = false; const NON_UPDATE_RETURN_TYPE: bool = false; const NO_LIFETIME: bool = false; const SINGLETON: bool = false; const DATA: bool = false; const DB: bool = false; const CYCLE_FN: bool = false; const CYCLE_INITIAL: bool = false; const CYCLE_RESULT: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = false; const ID: bool = false; const REVISIONS: bool = false; const HEAP_SIZE: bool = false; const SELF_TY: bool = false; } struct StructMacro { hygiene: Hygiene, _args: Options, struct_item: syn::ItemStruct, } #[allow(non_snake_case)] impl StructMacro { fn try_expand(self) -> syn::Result { let ident = self.struct_item.ident.clone(); let zalsa = self.hygiene.ident("zalsa"); let zalsa_struct = self.hygiene.ident("zalsa_struct"); let CACHE = self.hygiene.ident("CACHE"); let ingredient = self.hygiene.ident("ingredient"); let struct_item = self.struct_item; Ok(quote! { #struct_item salsa::plumbing::setup_accumulator_impl! { Struct: #ident, unused_names: [ #zalsa, #zalsa_struct, #CACHE, #ingredient, ] } }) } } salsa-macros-0.23.0/src/db.rs000064400000000000000000000115621046102023000140220ustar 00000000000000use proc_macro2::TokenStream; use syn::parse::Nothing; use crate::hygiene::Hygiene; use crate::token_stream_with_error; // Source: // // #[salsa::db] // pub struct Database { // storage: salsa::Storage, // } pub(crate) fn db( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let _nothing = syn::parse_macro_input!(args as Nothing); let hygiene = Hygiene::from1(&input); let item = parse_macro_input!(input as syn::Item); let db_macro = DbMacro { hygiene }; match db_macro.try_db(item) { Ok(v) => crate::debug::dump_tokens("db", v).into(), Err(e) => token_stream_with_error(input, e), } } struct DbMacro { hygiene: Hygiene, } #[allow(non_snake_case)] impl DbMacro { fn try_db(self, input: syn::Item) -> syn::Result { match input { syn::Item::Struct(input) => { let has_storage_impl = self.has_storage_impl(&input)?; Ok(quote! { #has_storage_impl #input }) } syn::Item::Trait(mut input) => { self.add_salsa_view_method(&mut input)?; Ok(quote! { #input }) } syn::Item::Impl(mut input) => { self.add_salsa_view_method_impl(&mut input)?; Ok(quote! { #input }) } _ => Err(syn::Error::new_spanned( input, "`db` must be applied to a struct, trait, or impl", )), } } fn find_storage_field(&self, input: &syn::ItemStruct) -> syn::Result { let storage = "storage"; for field in input.fields.iter() { if let Some(i) = &field.ident { if i == storage { return Ok(i.clone()); } } else { return Err(syn::Error::new_spanned( field, "database struct must be a braced struct (`{}`) with a field named `storage`", )); } } Err(syn::Error::new_spanned( &input.ident, "database struct must be a braced struct (`{}`) with a field named `storage`", )) } fn has_storage_impl(&self, input: &syn::ItemStruct) -> syn::Result { let storage = self.find_storage_field(input)?; let db = &input.ident; let zalsa = self.hygiene.ident("zalsa"); Ok(quote! { #[allow(clippy::all)] #[allow(dead_code)] const _: () = { use salsa::plumbing as #zalsa; unsafe impl #zalsa::HasStorage for #db { #[inline(always)] fn storage(&self) -> &#zalsa::Storage { &self.#storage } #[inline(always)] fn storage_mut(&mut self) -> &mut #zalsa::Storage { &mut self.#storage } } }; }) } fn add_salsa_view_method(&self, input: &mut syn::ItemTrait) -> syn::Result<()> { let trait_name = &input.ident; input.items.push(parse_quote! { #[doc(hidden)] fn zalsa_register_downcaster(&self); }); let comment = format!(" Downcast a [`dyn Database`] to a [`dyn {trait_name}`]"); input.items.push(parse_quote! { #[doc = #comment] /// /// # Safety /// /// The input database must be of type `Self`. #[doc(hidden)] unsafe fn downcast(db: &dyn salsa::plumbing::Database) -> &dyn #trait_name where Self: Sized; }); Ok(()) } fn add_salsa_view_method_impl(&self, input: &mut syn::ItemImpl) -> syn::Result<()> { let Some((_, TraitPath, _)) = &input.trait_ else { return Err(syn::Error::new_spanned( &input.self_ty, "impl must be on a trait", )); }; input.items.push(parse_quote! { #[doc(hidden)] #[inline(always)] fn zalsa_register_downcaster(&self) { salsa::plumbing::views(self).add(::downcast); } }); input.items.push(parse_quote! { #[doc(hidden)] #[inline(always)] unsafe fn downcast(db: &dyn salsa::plumbing::Database) -> &dyn #TraitPath where Self: Sized { debug_assert_eq!(db.type_id(), ::core::any::TypeId::of::()); // SAFETY: The input database must be of type `Self`. unsafe { &*salsa::plumbing::transmute_data_ptr::(db) } } }); Ok(()) } } salsa-macros-0.23.0/src/db_lifetime.rs000064400000000000000000000042541046102023000157000ustar 00000000000000//! Helper functions for working with fns, structs, and other generic things //! that are allowed to have a `'db` lifetime. use proc_macro2::Span; use syn::spanned::Spanned; /// Normally we try to use whatever lifetime parameter the user gave us /// to represent `'db`; but if they didn't give us one, we need to use a default /// name. We choose `'db`. pub(crate) fn default_db_lifetime(span: Span) -> syn::Lifetime { syn::Lifetime { apostrophe: span, ident: syn::Ident::new("db", span), } } /// Require that either there are no generics or exactly one lifetime parameter. pub(crate) fn require_optional_db_lifetime(generics: &syn::Generics) -> syn::Result<()> { if generics.params.is_empty() { return Ok(()); } require_db_lifetime(generics)?; Ok(()) } /// Require that either there is exactly one lifetime parameter. pub(crate) fn require_db_lifetime(generics: &syn::Generics) -> syn::Result<()> { if generics.params.is_empty() { return Err(syn::Error::new_spanned( generics, "this definition must have a `'db` lifetime", )); } for (param, index) in generics.params.iter().zip(0..) { let error = match param { syn::GenericParam::Lifetime(_) => index > 0, syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => true, }; if error { return Err(syn::Error::new_spanned( param, "only a single lifetime parameter is accepted", )); } } Ok(()) } /// Return the `'db` lifetime given by the user, or a default. /// The generics ought to have been checked with `require_db_lifetime` already. pub(crate) fn db_lifetime(generics: &syn::Generics) -> syn::Lifetime { if let Some(lt) = generics.lifetimes().next() { lt.lifetime.clone() } else { default_db_lifetime(generics.span()) } } pub(crate) fn require_no_generics(generics: &syn::Generics) -> syn::Result<()> { if let Some(param) = generics.params.iter().next() { return Err(syn::Error::new_spanned( param, "generic parameters not allowed here", )); } Ok(()) } salsa-macros-0.23.0/src/debug.rs000064400000000000000000000024061046102023000145200ustar 00000000000000use std::io::Write; use std::process::{Command, Stdio}; use std::sync::OnceLock; use proc_macro2::TokenStream; static SALSA_DEBUG_MACRO: OnceLock> = OnceLock::new(); pub(crate) fn debug_enabled(input_name: impl ToString) -> bool { let Some(env_name) = SALSA_DEBUG_MACRO.get_or_init(|| std::env::var("SALSA_DEBUG_MACRO").ok()) else { return false; }; let input_name = input_name.to_string(); env_name == "*" || env_name == &input_name[..] } pub(crate) fn dump_tokens(input_name: impl ToString, tokens: TokenStream) -> TokenStream { if debug_enabled(input_name) { let token_string = tokens.to_string(); let _: Result<(), ()> = Command::new("rustfmt") .arg("--emit=stdout") .stdin(Stdio::piped()) .spawn() .and_then(|mut rustfmt| { rustfmt .stdin .take() .unwrap() .write_all(token_string.as_bytes())?; rustfmt.wait_with_output() }) .map(|output| eprintln!("{}", String::from_utf8_lossy(&output.stdout))) .or_else(|_| { eprintln!("{token_string}"); Ok(()) }); } tokens } salsa-macros-0.23.0/src/fn_util.rs000064400000000000000000000026131046102023000150720ustar 00000000000000use crate::hygiene::Hygiene; use crate::xform::ChangeLt; /// Returns a vector of ids representing the function arguments. /// Prefers to reuse the names given by the user, if possible. pub fn input_ids(hygiene: &Hygiene, sig: &syn::Signature, skip: usize) -> Vec { sig.inputs .iter() .skip(skip) .zip(0..) .map(|(input, index)| { if let syn::FnArg::Typed(typed) = input { if let syn::Pat::Ident(ident) = &*typed.pat { return ident.ident.clone(); } } hygiene.ident(&format!("input{index}")) }) .collect() } pub fn input_tys(sig: &syn::Signature, skip: usize) -> syn::Result> { sig.inputs .iter() .skip(skip) .map(|input| { if let syn::FnArg::Typed(typed) = input { Ok(&*typed.ty) } else { Err(syn::Error::new_spanned(input, "unexpected receiver")) } }) .collect() } pub fn output_ty(db_lt: Option<&syn::Lifetime>, sig: &syn::Signature) -> syn::Result { match &sig.output { syn::ReturnType::Default => Ok(parse_quote!(())), syn::ReturnType::Type(_, ty) => match db_lt { Some(db_lt) => Ok(ChangeLt::elided_to(db_lt).in_type(ty)), None => Ok(syn::Type::clone(ty)), }, } } salsa-macros-0.23.0/src/hygiene.rs000064400000000000000000000037671046102023000150750ustar 00000000000000use std::collections::HashSet; use quote::ToTokens; pub struct Hygiene { user_tokens: HashSet, } impl Hygiene { pub fn from1(tokens: &proc_macro::TokenStream) -> Self { let mut user_tokens = HashSet::new(); push_idents1(tokens.clone(), &mut user_tokens); Self { user_tokens } } pub fn from2(tokens: &impl ToTokens) -> Self { let mut user_tokens = HashSet::new(); push_idents2(tokens.to_token_stream(), &mut user_tokens); Self { user_tokens } } } fn push_idents1(input: proc_macro::TokenStream, user_tokens: &mut HashSet) { input.into_iter().for_each(|token| match token { proc_macro::TokenTree::Group(g) => { push_idents1(g.stream(), user_tokens); } proc_macro::TokenTree::Ident(ident) => { user_tokens.insert(ident.to_string()); } proc_macro::TokenTree::Punct(_) => (), proc_macro::TokenTree::Literal(_) => (), }) } fn push_idents2(input: proc_macro2::TokenStream, user_tokens: &mut HashSet) { input.into_iter().for_each(|token| match token { proc_macro2::TokenTree::Group(g) => { push_idents2(g.stream(), user_tokens); } proc_macro2::TokenTree::Ident(ident) => { user_tokens.insert(ident.to_string()); } proc_macro2::TokenTree::Punct(_) => (), proc_macro2::TokenTree::Literal(_) => (), }) } impl Hygiene { /// Generates an identifier similar to `text` but /// distinct from any identifiers that appear in the user's /// code. pub(crate) fn ident(&self, text: &str) -> syn::Ident { // Make the default be `foo_` rather than `foo` -- this helps detect // cases where people wrote `foo` instead of `#foo` or `$foo` in the generated code. let mut buffer = format!("{text}_"); while self.user_tokens.contains(&buffer) { buffer.push('_'); } syn::Ident::new(&buffer, proc_macro2::Span::call_site()) } } salsa-macros-0.23.0/src/input.rs000064400000000000000000000113531046102023000145720ustar 00000000000000use proc_macro2::TokenStream; use crate::hygiene::Hygiene; use crate::options::Options; use crate::salsa_struct::{SalsaStruct, SalsaStructAllowedOptions}; use crate::token_stream_with_error; /// 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 input( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let args = syn::parse_macro_input!(args as InputArgs); 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 InputArgs = Options; struct InputStruct; impl crate::options::AllowedOptions for InputStruct { const RETURNS: bool = false; const SPECIFY: bool = false; const NO_EQ: bool = false; const DEBUG: bool = true; const NO_LIFETIME: bool = false; const NON_UPDATE_RETURN_TYPE: bool = false; const SINGLETON: bool = true; const DATA: bool = true; const DB: bool = false; const CYCLE_FN: bool = false; const CYCLE_INITIAL: bool = false; const CYCLE_RESULT: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; const ID: bool = false; const REVISIONS: bool = false; const HEAP_SIZE: bool = false; const SELF_TY: bool = false; } impl SalsaStructAllowedOptions for InputStruct { const KIND: &'static str = "input"; const ALLOW_MAYBE_UPDATE: bool = false; const ALLOW_TRACKED: bool = false; const HAS_LIFETIME: bool = false; const ELIDABLE_LIFETIME: bool = false; const ALLOW_DEFAULT: bool = true; } struct Macro { hygiene: Hygiene, args: InputArgs, 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 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_setter_ids = salsa_struct.field_setter_ids(); let required_fields = salsa_struct.required_fields(); let field_options = salsa_struct.field_options(); let field_tys = salsa_struct.field_tys(); let field_durability_ids = salsa_struct.field_durability_ids(); let field_attrs = salsa_struct.field_attrs(); let is_singleton = self.args.singleton.is_some(); 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 Builder = self.hygiene.ident("Builder"); let CACHE = self.hygiene.ident("CACHE"); let Db = self.hygiene.ident("Db"); Ok(crate::debug::dump_tokens( struct_ident, quote! { salsa::plumbing::setup_input_struct!( attrs: [#(#attrs),*], vis: #vis, Struct: #struct_ident, new_fn: #new_fn, field_options: [#(#field_options),*], field_ids: [#(#field_ids),*], field_getters: [#(#field_vis #field_getter_ids),*], field_setters: [#(#field_vis #field_setter_ids),*], field_tys: [#(#field_tys),*], field_indices: [#(#field_indices),*], field_attrs: [#([#(#field_attrs),*]),*], required_fields: [#(#required_fields),*], field_durability_ids: [#(#field_durability_ids),*], num_fields: #num_fields, is_singleton: #is_singleton, generate_debug_impl: #generate_debug_impl, unused_names: [ #zalsa, #zalsa_struct, #Configuration, #Builder, #CACHE, #Db, ] ); }, )) } } salsa-macros-0.23.0/src/interned.rs000064400000000000000000000126571046102023000152530ustar 00000000000000use proc_macro2::TokenStream; use crate::hygiene::Hygiene; use crate::options::Options; use crate::salsa_struct::{SalsaStruct, SalsaStructAllowedOptions}; use crate::{db_lifetime, token_stream_with_error}; /// 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( 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; struct InternedStruct; impl crate::options::AllowedOptions for InternedStruct { const RETURNS: bool = false; const SPECIFY: bool = false; const NO_EQ: bool = false; const DEBUG: bool = true; const NO_LIFETIME: bool = true; const NON_UPDATE_RETURN_TYPE: bool = false; const SINGLETON: bool = true; const DATA: bool = true; const DB: bool = false; const CYCLE_FN: bool = false; const CYCLE_INITIAL: bool = false; const CYCLE_RESULT: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; const ID: bool = true; const REVISIONS: bool = true; const HEAP_SIZE: bool = false; const SELF_TY: bool = false; } impl SalsaStructAllowedOptions for InternedStruct { const KIND: &'static str = "interned"; const ALLOW_MAYBE_UPDATE: bool = false; const ALLOW_TRACKED: bool = false; const HAS_LIFETIME: bool = true; const ELIDABLE_LIFETIME: bool = true; 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 field_unused_attrs = salsa_struct.field_attrs(); let generate_debug_impl = salsa_struct.generate_debug_impl(); let has_lifetime = salsa_struct.generate_lifetime(); let id = salsa_struct.id(); let revisions = salsa_struct.revisions(); let (db_lt_arg, cfg, interior_lt) = if has_lifetime { ( Some(db_lt.clone()), quote!(#struct_ident<'static>), db_lt.clone(), ) } else { let span = syn::spanned::Spanned::span(&self.struct_item.generics); let static_lifetime = syn::Lifetime { apostrophe: span, ident: syn::Ident::new("static", span), }; (None, quote!(#struct_ident), static_lifetime) }; 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!( attrs: [#(#attrs),*], vis: #vis, Struct: #struct_ident, StructData: #struct_data_ident, StructWithStatic: #cfg, db_lt: #db_lt, db_lt_arg: #db_lt_arg, id: #id, revisions: #(#revisions)*, interior_lt: #interior_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),*], field_attrs: [#([#(#field_unused_attrs),*]),*], num_fields: #num_fields, generate_debug_impl: #generate_debug_impl, unused_names: [ #zalsa, #zalsa_struct, #Configuration, #CACHE, #Db, ] ); }, )) } } salsa-macros-0.23.0/src/lib.rs000064400000000000000000000047631046102023000142100ustar 00000000000000//! This crate provides salsa's macros and attributes. #![recursion_limit = "256"] #[macro_use] extern crate quote; use proc_macro::TokenStream; macro_rules! parse_quote { ($($inp:tt)*) => { { let tt = quote!{$($inp)*}; syn::parse2(tt.clone()).unwrap_or_else(|err| { panic!("failed to parse `{}` at {}:{}:{}: {}", tt, file!(), line!(), column!(), err) }) } } } /// Similar to `syn::parse_macro_input`, however, when a parse error is encountered, it will return /// the input token stream in addition to the error. This will make it so that rust-analyzer can work /// with incomplete code. macro_rules! parse_macro_input { ($tokenstream:ident as $ty:ty) => { match syn::parse::<$ty>($tokenstream.clone()) { Ok(data) => data, Err(err) => { return $crate::token_stream_with_error($tokenstream, err); } } }; } mod accumulator; mod db; mod db_lifetime; mod debug; mod fn_util; mod hygiene; mod input; mod interned; mod options; mod salsa_struct; mod supertype; mod tracked; mod tracked_fn; mod tracked_impl; mod tracked_struct; mod update; mod xform; #[proc_macro_attribute] pub fn accumulator(args: TokenStream, input: TokenStream) -> TokenStream { accumulator::accumulator(args, input) } #[proc_macro_attribute] pub fn db(args: TokenStream, input: TokenStream) -> TokenStream { db::db(args, input) } #[proc_macro_attribute] pub fn interned(args: TokenStream, input: TokenStream) -> TokenStream { interned::interned(args, input) } #[proc_macro_derive(Supertype)] pub fn supertype(input: TokenStream) -> TokenStream { supertype::supertype(input) } #[proc_macro_attribute] pub fn input(args: TokenStream, input: TokenStream) -> TokenStream { input::input(args, input) } #[proc_macro_attribute] pub fn tracked(args: TokenStream, input: TokenStream) -> TokenStream { tracked::tracked(args, input) } #[proc_macro_derive(Update, attributes(update))] pub fn update(input: TokenStream) -> TokenStream { let item = parse_macro_input!(input as syn::DeriveInput); match update::update_derive(item) { Ok(tokens) => tokens.into(), Err(error) => error.into_compile_error().into(), } } pub(crate) fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream { tokens.extend(TokenStream::from(error.into_compile_error())); tokens } mod kw { syn::custom_keyword!(with); syn::custom_keyword!(maybe_update); } salsa-macros-0.23.0/src/options.rs000064400000000000000000000506701046102023000151330ustar 00000000000000use std::marker::PhantomData; use syn::ext::IdentExt; use syn::parenthesized; use syn::spanned::Spanned; /// "Options" are flags that can be supplied to the various salsa related /// macros. They are listed like `(ref, no_eq, foo=bar)` etc. The commas /// 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 `returns` option is used to configure the "return mode" for the field/function. /// This may be one of `copy`, `clone`, `ref`, `as_ref`, `as_deref`. /// /// If this is `Some`, the value is the ident representing the selected mode. pub returns: Option, /// The `no_eq` option is used to signal that a given field does not implement /// the `Eq` trait and cannot be compared for equality. /// /// If this is `Some`, the value is the `no_eq` identifier. pub no_eq: Option, /// Signal we should generate a `Debug` impl. /// /// If this is `Some`, the value is the `debug` identifier. pub debug: Option, /// Signal we should not include the `'db` lifetime. /// /// If this is `Some`, the value is the `no_lifetime` identifier. pub no_lifetime: 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). /// /// If this is `Some`, the value is the `specify` identifier. pub specify: Option, /// The `non_update_return_type` option is used to signal that a tracked function's /// return type does not require `Update` to be implemented. This is unsafe and /// generally discouraged as it allows for dangling references. /// /// If this is `Some`, the value is the `non_update_return_type` identifier. pub non_update_return_type: Option, /// The `db = ` option is used to indicate the db. /// /// If this is `Some`, the value is the ``. pub db_path: Option, /// The `cycle_fn = ` option is used to indicate the cycle recovery function. /// /// If this is `Some`, the value is the ``. pub cycle_fn: Option, /// The `cycle_initial = ` option is the initial value for cycle iteration. /// /// If this is `Some`, the value is the ``. pub cycle_initial: Option, /// The `cycle_result = ` option is the result for non-fixpoint cycle. /// /// If this is `Some`, the value is the ``. pub cycle_result: Option, /// The `data = ` option is used to define the name of the data type for an interned /// struct. /// /// If this is `Some`, the value is the ``. pub data: Option, /// The `lru = ` option is used to set the lru capacity for a tracked function. /// /// If this is `Some`, the value is the ``. pub lru: Option, /// The `constructor = ` option lets the user specify the name of /// the constructor of a salsa struct. /// /// 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 ID must implement `salsa::plumbing::AsId` and `salsa::plumbing::FromId`. /// If this is `Some`, the value is the ``. pub id: Option, /// The `revisions = ` option is used to set the minimum number of revisions /// to keep a value interned. /// /// This is stored as a `syn::Expr` to support `usize::MAX`. pub revisions: Option, /// The `heap_size = ` option can be used to track heap memory usage of memoized /// values. /// /// If this is `Some`, the value is the provided `heap_size` function. pub heap_size_fn: Option, /// The `self_ty = ` option is used to set the the self type of the tracked impl for tracked /// functions. This is merely used to refine the query name. pub self_ty: Option, /// Remember the `A` parameter, which plays no role after parsing. phantom: PhantomData, } impl Default for Options { fn default() -> Self { Self { returns: Default::default(), specify: Default::default(), non_update_return_type: Default::default(), no_eq: Default::default(), debug: Default::default(), no_lifetime: Default::default(), db_path: Default::default(), cycle_fn: Default::default(), cycle_initial: Default::default(), cycle_result: Default::default(), data: Default::default(), constructor_name: Default::default(), phantom: Default::default(), lru: Default::default(), singleton: Default::default(), id: Default::default(), revisions: Default::default(), heap_size_fn: Default::default(), self_ty: Default::default(), } } } /// These flags determine which options are allowed in a given context pub(crate) trait AllowedOptions { const RETURNS: bool; const SPECIFY: bool; const NO_EQ: bool; const DEBUG: bool; const NO_LIFETIME: bool; const NON_UPDATE_RETURN_TYPE: bool; const SINGLETON: bool; const DATA: bool; const DB: bool; const CYCLE_FN: bool; const CYCLE_INITIAL: bool; const CYCLE_RESULT: bool; const LRU: bool; const CONSTRUCTOR_NAME: bool; const ID: bool; const REVISIONS: bool; const HEAP_SIZE: bool; const SELF_TY: bool; } type Equals = syn::Token![=]; type Comma = syn::Token![,]; impl syn::parse::Parse for Options { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut options = Options::default(); while !input.is_empty() { let ident: syn::Ident = syn::Ident::parse_any(input)?; if ident == "returns" { let content; parenthesized!(content in input); let mode = syn::Ident::parse_any(&content)?; if A::RETURNS { if let Some(old) = options.returns.replace(mode) { return Err(syn::Error::new( old.span(), "option `returns` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`returns` option not allowed here", )); } } else if ident == "no_eq" { if A::NO_EQ { if let Some(old) = options.no_eq.replace(ident) { return Err(syn::Error::new(old.span(), "option `no_eq` provided twice")); } } else { return Err(syn::Error::new( ident.span(), "`no_eq` option not allowed here", )); } } else if ident == "debug" { if A::DEBUG { if let Some(old) = options.debug.replace(ident) { return Err(syn::Error::new(old.span(), "option `debug` provided twice")); } } else { return Err(syn::Error::new( ident.span(), "`debug` option not allowed here", )); } } else if ident == "no_lifetime" { if A::NO_LIFETIME { if let Some(old) = options.no_lifetime.replace(ident) { return Err(syn::Error::new( old.span(), "option `no_lifetime` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`no_lifetime` option not allowed here", )); } } else if ident == "unsafe" { if A::NON_UPDATE_RETURN_TYPE { let content; parenthesized!(content in input); let ident = syn::Ident::parse_any(&content)?; if ident == "non_update_return_type" { if let Some(old) = options.non_update_return_type.replace(ident) { return Err(syn::Error::new( old.span(), "option `non_update_return_type` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "expected `non_update_return_type`", )); } } else { return Err(syn::Error::new( ident.span(), "`unsafe` options not allowed here", )); } } else if ident == "singleton" { if A::SINGLETON { if let Some(old) = options.singleton.replace(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) = options.specify.replace(ident) { return Err(syn::Error::new( old.span(), "option `specify` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`specify` option not allowed here", )); } } else if ident == "db" { if A::DB { let _eq = Equals::parse(input)?; let path = syn::Path::parse(input)?; if let Some(old) = options.db_path.replace(path) { return Err(syn::Error::new(old.span(), "option `db` provided twice")); } } else { return Err(syn::Error::new( ident.span(), "`db` option not allowed here", )); } } else if ident == "cycle_fn" { if A::CYCLE_FN { let _eq = Equals::parse(input)?; let path = syn::Path::parse(input)?; if let Some(old) = options.cycle_fn.replace(path) { return Err(syn::Error::new( old.span(), "option `cycle_fn` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`cycle_fn` option not allowed here", )); } } else if ident == "cycle_initial" { if A::CYCLE_INITIAL { let _eq = Equals::parse(input)?; let path = syn::Path::parse(input)?; if let Some(old) = options.cycle_initial.replace(path) { return Err(syn::Error::new( old.span(), "option `cycle_initial` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`cycle_initial` option not allowed here", )); } } else if ident == "cycle_result" { if A::CYCLE_RESULT { let _eq = Equals::parse(input)?; let expr = syn::Expr::parse(input)?; if let Some(old) = options.cycle_result.replace(expr) { return Err(syn::Error::new( old.span(), "option `cycle_result` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`cycle_result` option not allowed here", )); } } else if ident == "data" { if A::DATA { let _eq = Equals::parse(input)?; let ident = syn::Ident::parse(input)?; if let Some(old) = options.data.replace(ident) { return Err(syn::Error::new(old.span(), "option `data` provided twice")); } } else { return Err(syn::Error::new( ident.span(), "`data` option not allowed here", )); } } else if ident == "lru" { if A::LRU { let _eq = Equals::parse(input)?; let lit = syn::LitInt::parse(input)?; let value = lit.base10_parse::()?; if let Some(old) = options.lru.replace(value) { return Err(syn::Error::new(old.span(), "option `lru` provided twice")); } } else { return Err(syn::Error::new( ident.span(), "`lru` option not allowed here", )); } } else if ident == "constructor" { if A::CONSTRUCTOR_NAME { let _eq = Equals::parse(input)?; let ident = syn::Ident::parse(input)?; if let Some(old) = options.constructor_name.replace(ident) { return Err(syn::Error::new( old.span(), "option `constructor` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`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 if ident == "revisions" { if A::REVISIONS { let _eq = Equals::parse(input)?; let expr = syn::Expr::parse(input)?; if let Some(old) = options.revisions.replace(expr) { return Err(syn::Error::new( old.span(), "option `revisions` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`revisions` option not allowed here", )); } } else if ident == "heap_size" { if A::HEAP_SIZE { let _eq = Equals::parse(input)?; let path = syn::Path::parse(input)?; if let Some(old) = options.heap_size_fn.replace(path) { return Err(syn::Error::new( old.span(), "option `heap_size` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`heap_size` option not allowed here", )); } } else if ident == "self_ty" { if A::SELF_TY { let _eq = Equals::parse(input)?; let ty = syn::Type::parse(input)?; if let Some(old) = options.self_ty.replace(ty) { return Err(syn::Error::new( old.span(), "option `self_ty` provided twice", )); } } else { return Err(syn::Error::new( ident.span(), "`self_ty` option not allowed here", )); } } else { return Err(syn::Error::new( ident.span(), format!("unrecognized option `{ident}`"), )); } if input.is_empty() { break; } let _comma = Comma::parse(input)?; } Ok(options) } } impl quote::ToTokens for Options { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { returns, no_eq, debug, no_lifetime, singleton, specify, non_update_return_type, db_path, cycle_fn, cycle_initial, cycle_result, data, lru, constructor_name, id, revisions, heap_size_fn, self_ty, phantom: _, } = self; if let Some(returns) = returns { tokens.extend(quote::quote! { returns(#returns), }); }; if no_eq.is_some() { tokens.extend(quote::quote! { no_eq, }); } if debug.is_some() { tokens.extend(quote::quote! { debug, }); } if no_lifetime.is_some() { tokens.extend(quote::quote! { no_lifetime, }); } if singleton.is_some() { tokens.extend(quote::quote! { singleton, }); } if specify.is_some() { tokens.extend(quote::quote! { specify, }); } if non_update_return_type.is_some() { tokens.extend(quote::quote! { unsafe(non_update_return_type), }); } if let Some(db_path) = db_path { tokens.extend(quote::quote! { db = #db_path, }); } if let Some(cycle_fn) = cycle_fn { tokens.extend(quote::quote! { cycle_fn = #cycle_fn, }); } if let Some(cycle_initial) = cycle_initial { tokens.extend(quote::quote! { cycle_initial = #cycle_initial, }); } if let Some(cycle_result) = cycle_result { tokens.extend(quote::quote! { cycle_result = #cycle_result, }); } if let Some(data) = data { tokens.extend(quote::quote! { data = #data, }); } if let Some(lru) = lru { tokens.extend(quote::quote! { lru = #lru, }); } if let Some(constructor_name) = constructor_name { tokens.extend(quote::quote! { constructor = #constructor_name, }); } if let Some(id) = id { tokens.extend(quote::quote! { id = #id, }); } if let Some(revisions) = revisions { tokens.extend(quote::quote! { revisions = #revisions, }); } if let Some(heap_size_fn) = heap_size_fn { tokens.extend(quote::quote! { heap_size_fn = #heap_size_fn, }); } if let Some(self_ty) = self_ty { tokens.extend(quote::quote! { self_ty = #self_ty, }); } } } salsa-macros-0.23.0/src/salsa_struct.rs000064400000000000000000000360411046102023000161430ustar 00000000000000//! Common code for `#[salsa::interned]`, `#[salsa::input]`, and //! `#[salsa::tracked]` decorators. //! //! Example of usage: //! //! ```rust,ignore //! #[salsa::interned(jar = Jar0, data = TyData0)] //! #[derive(Eq, PartialEq, Hash, Debug, Clone)] //! struct Ty0 { //! field1: Type1, //! #[ref] field2: Type2, //! ... //! } //! ``` //! For an interned or entity struct `Foo`, we generate: //! //! * the actual struct: `struct Foo(Id);` //! * constructor function: `impl Foo { fn new(db: &crate::Db, field1: Type1, ..., fieldN: TypeN) -> Self { ... } } //! * field accessors: `impl Foo { fn field1(&self) -> Type1 { self.field1.clone() } }` //! * if the field is `ref`, we generate `fn field1(&self) -> &Type1` //! //! Only if there are no `ref` fields: //! //! * the data type: `struct FooData { field1: Type1, ... }` or `enum FooData { ... }` //! * 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 proc_macro2::{Ident, Literal, Span, TokenStream}; use syn::parse::ParseStream; use syn::{ext::IdentExt, spanned::Spanned}; use crate::db_lifetime; use crate::options::{AllowedOptions, Options}; pub(crate) struct SalsaStruct<'s, A: SalsaStructAllowedOptions> { struct_item: &'s syn::ItemStruct, args: &'s Options, fields: Vec>, } pub(crate) trait SalsaStructAllowedOptions: AllowedOptions { /// The kind of struct (e.g., interned, input, tracked). const KIND: &'static str; /// Are `#[maybe_update]` fields allowed? const ALLOW_MAYBE_UPDATE: bool; /// Are `#[tracked]` fields allowed? const ALLOW_TRACKED: bool; /// Does this kind of struct have a `'db` lifetime? const HAS_LIFETIME: bool; /// Can this struct elide the `'db` lifetime? const ELIDABLE_LIFETIME: bool; /// Are `#[default]` fields allowed? const ALLOW_DEFAULT: bool; } pub(crate) struct SalsaField<'s> { pub(crate) field: &'s syn::Field, pub(crate) has_tracked_attr: bool, pub(crate) has_default_attr: bool, pub(crate) returns: syn::Ident, pub(crate) has_no_eq_attr: bool, pub(crate) maybe_update_attr: Option<(syn::Path, syn::Expr)>, get_name: syn::Ident, set_name: syn::Ident, unknown_attrs: Vec<&'s syn::Attribute>, } const BANNED_FIELD_NAMES: &[&str] = &["from", "new"]; const ALLOWED_RETURN_MODES: &[&str] = &["copy", "clone", "ref", "deref", "as_ref", "as_deref"]; #[allow(clippy::type_complexity)] pub(crate) const FIELD_OPTION_ATTRIBUTES: &[( &str, fn(&syn::Attribute, &mut SalsaField) -> syn::Result<()>, )] = &[ ("tracked", |_, ef| { ef.has_tracked_attr = true; Ok(()) }), ("default", |_, ef| { ef.has_default_attr = true; Ok(()) }), ("returns", |attr, ef| { ef.returns = attr.parse_args_with(syn::Ident::parse_any)?; Ok(()) }), ("no_eq", |_, ef| { ef.has_no_eq_attr = true; Ok(()) }), ("get", |attr, ef| { ef.get_name = attr.parse_args()?; Ok(()) }), ("set", |attr, ef| { ef.set_name = attr.parse_args()?; Ok(()) }), ("maybe_update", |attr, ef| { ef.maybe_update_attr = Some(attr.parse_args_with(|parser: ParseStream| { let expr = parser.parse::()?; Ok((attr.path().clone(), expr)) })?); Ok(()) }), ]; impl<'s, A> SalsaStruct<'s, A> where A: SalsaStructAllowedOptions, { pub fn new(struct_item: &'s syn::ItemStruct, args: &'s Options) -> syn::Result { let syn::Fields::Named(n) = &struct_item.fields else { return Err(syn::Error::new_spanned( &struct_item.ident, "must have named fields for a struct", )); }; let fields = n .named .iter() .map(SalsaField::new) .collect::>()?; let this = Self { struct_item, args, fields, }; this.maybe_disallow_maybe_update_fields()?; this.maybe_disallow_tracked_fields()?; this.maybe_disallow_default_fields()?; this.check_generics()?; Ok(this) } /// Returns the `constructor_name` in `Options` if it is `Some`, else `new` pub(crate) fn constructor_name(&self) -> syn::Ident { match self.args.constructor_name.clone() { Some(name) => name, None => Ident::new("new", self.struct_item.ident.span()), } } /// Returns the `id` in `Options` if it is `Some`, else `salsa::Id`. pub(crate) fn id(&self) -> syn::Path { match &self.args.id { Some(id) => id.clone(), None => parse_quote!(salsa::Id), } } /// Returns the `revisions` in `Options` as an optional iterator. pub(crate) fn revisions(&self) -> impl Iterator + '_ { self.args.revisions.iter() } /// Disallow `#[tracked]` attributes on the fields of this struct. /// /// If an `#[tracked]` field is found, return an error. /// /// # Parameters /// /// * `kind`, the attribute name (e.g., `input` or `interned`) fn maybe_disallow_maybe_update_fields(&self) -> syn::Result<()> { if A::ALLOW_MAYBE_UPDATE { return Ok(()); } // Check if any field has the `#[maybe_update]` attribute. for ef in &self.fields { if ef.maybe_update_attr.is_some() { return Err(syn::Error::new_spanned( ef.field, format!( "`#[maybe_update]` cannot be used with `#[salsa::{}]`", A::KIND ), )); } } Ok(()) } /// Disallow `#[tracked]` attributes on the fields of this struct. /// /// If an `#[tracked]` field is found, return an error. /// /// # Parameters /// /// * `kind`, the attribute name (e.g., `input` or `interned`) fn maybe_disallow_tracked_fields(&self) -> syn::Result<()> { if A::ALLOW_TRACKED { return Ok(()); } // Check if any field has the `#[tracked]` attribute. for ef in &self.fields { if ef.has_tracked_attr { return Err(syn::Error::new_spanned( ef.field, format!("`#[tracked]` cannot be used with `#[salsa::{}]`", A::KIND), )); } } Ok(()) } /// Disallow `#[default]` attributes on the fields of this struct. /// /// If an `#[default]` field is found, return an error. /// /// # Parameters /// /// * `kind`, the attribute name (e.g., `input` or `interned`) fn maybe_disallow_default_fields(&self) -> syn::Result<()> { if A::ALLOW_DEFAULT { return Ok(()); } // Check if any field has the `#[default]` attribute. for ef in &self.fields { if ef.has_default_attr { return Err(syn::Error::new_spanned( ef.field, format!("`#[default]` cannot be used with `#[salsa::{}]`", A::KIND), )); } } Ok(()) } /// Check that the generic parameters look as expected for this kind of struct. fn check_generics(&self) -> syn::Result<()> { if A::HAS_LIFETIME { if !A::ELIDABLE_LIFETIME { db_lifetime::require_db_lifetime(&self.struct_item.generics) } else { Ok(()) } } else { db_lifetime::require_no_generics(&self.struct_item.generics) } } pub(crate) fn field_ids(&self) -> Vec<&syn::Ident> { self.fields .iter() .map(|f| f.field.ident.as_ref().unwrap()) .collect() } pub(crate) fn tracked_ids(&self) -> Vec<&syn::Ident> { self.tracked_fields_iter() .map(|(_, f)| f.field.ident.as_ref().unwrap()) .collect() } pub(crate) fn field_indices(&self) -> Vec { (0..self.fields.len()) .map(Literal::usize_unsuffixed) .collect() } pub(crate) fn tracked_field_indices(&self) -> Vec { self.tracked_fields_iter() .map(|(index, _)| Literal::usize_unsuffixed(index)) .collect() } pub(crate) fn untracked_field_indices(&self) -> Vec { self.untracked_fields_iter() .map(|(index, _)| Literal::usize_unsuffixed(index)) .collect() } pub(crate) fn num_fields(&self) -> Literal { Literal::usize_unsuffixed(self.fields.len()) } pub(crate) fn num_tracked_fields(&self) -> Literal { Literal::usize_unsuffixed(self.tracked_fields_iter().count()) } pub(crate) fn required_fields(&self) -> Vec { self.fields .iter() .filter_map(|f| { if f.has_default_attr { None } else { let ident = f.field.ident.as_ref().unwrap(); let ty = &f.field.ty; Some(quote!(#ident #ty)) } }) .collect() } pub(crate) fn field_vis(&self) -> Vec<&syn::Visibility> { self.fields.iter().map(|f| &f.field.vis).collect() } pub(crate) fn tracked_vis(&self) -> Vec<&syn::Visibility> { self.tracked_fields_iter() .map(|(_, f)| &f.field.vis) .collect() } pub(crate) fn untracked_vis(&self) -> Vec<&syn::Visibility> { self.untracked_fields_iter() .map(|(_, f)| &f.field.vis) .collect() } pub(crate) fn field_getter_ids(&self) -> Vec<&syn::Ident> { self.fields.iter().map(|f| &f.get_name).collect() } pub(crate) fn tracked_getter_ids(&self) -> Vec<&syn::Ident> { self.tracked_fields_iter() .map(|(_, f)| &f.get_name) .collect() } pub(crate) fn untracked_getter_ids(&self) -> Vec<&syn::Ident> { self.untracked_fields_iter() .map(|(_, f)| &f.get_name) .collect() } pub(crate) fn field_setter_ids(&self) -> Vec<&syn::Ident> { self.fields.iter().map(|f| &f.set_name).collect() } pub(crate) fn field_durability_ids(&self) -> Vec { self.fields .iter() .map(|f| quote::format_ident!("{}_durability", f.field.ident.as_ref().unwrap())) .collect() } pub(crate) fn field_tys(&self) -> Vec<&syn::Type> { self.fields.iter().map(|f| &f.field.ty).collect() } pub(crate) fn tracked_tys(&self) -> Vec<&syn::Type> { self.tracked_fields_iter() .map(|(_, f)| &f.field.ty) .collect() } pub(crate) fn untracked_tys(&self) -> Vec<&syn::Type> { self.untracked_fields_iter() .map(|(_, f)| &f.field.ty) .collect() } pub(crate) fn field_indexed_tys(&self) -> Vec { self.fields .iter() .enumerate() .map(|(i, _)| quote::format_ident!("T{i}")) .collect() } pub(crate) fn field_attrs(&self) -> Vec<&[&syn::Attribute]> { self.fields.iter().map(|f| &*f.unknown_attrs).collect() } pub(crate) fn tracked_field_attrs(&self) -> Vec<&[&syn::Attribute]> { self.tracked_fields_iter() .map(|f| &*f.1.unknown_attrs) .collect() } pub(crate) fn untracked_field_attrs(&self) -> Vec<&[&syn::Attribute]> { self.untracked_fields_iter() .map(|f| &*f.1.unknown_attrs) .collect() } pub(crate) fn field_options(&self) -> Vec { self.fields.iter().map(SalsaField::options).collect() } pub(crate) fn tracked_options(&self) -> Vec { self.tracked_fields_iter() .map(|(_, f)| f.options()) .collect() } pub(crate) fn untracked_options(&self) -> Vec { self.untracked_fields_iter() .map(|(_, f)| f.options()) .collect() } pub fn generate_debug_impl(&self) -> bool { self.args.debug.is_some() } pub fn generate_lifetime(&self) -> bool { self.args.no_lifetime.is_none() } pub fn tracked_fields_iter(&self) -> impl Iterator)> { self.fields .iter() .enumerate() .filter(|(_, f)| f.has_tracked_attr) } pub fn untracked_fields_iter(&self) -> impl Iterator)> { self.fields .iter() .enumerate() .filter(|(_, f)| !f.has_tracked_attr) } } impl<'s> SalsaField<'s> { fn new(field: &'s syn::Field) -> syn::Result { let field_name = field.ident.as_ref().unwrap(); let field_name_str = field_name.to_string(); if BANNED_FIELD_NAMES.iter().any(|n| *n == field_name_str) { return Err(syn::Error::new( field_name.span(), format!("the field name `{field_name_str}` is disallowed in salsa structs",), )); } let get_name = Ident::new(&field_name_str, field_name.span()); let set_name = Ident::new(&format!("set_{field_name_str}",), field_name.span()); let returns = Ident::new("clone", field.span()); let mut result = SalsaField { field, has_tracked_attr: false, returns, has_default_attr: false, has_no_eq_attr: false, maybe_update_attr: None, get_name, set_name, unknown_attrs: Default::default(), }; // Scan the attributes and look for the salsa attributes: for attr in &field.attrs { let mut handled = false; for (fa, func) in FIELD_OPTION_ATTRIBUTES { if attr.path().is_ident(fa) { func(attr, &mut result)?; handled = true; break; } } if !handled { result.unknown_attrs.push(attr); } } // Validate return mode if !ALLOWED_RETURN_MODES .iter() .any(|mode| mode == &result.returns.to_string()) { return Err(syn::Error::new( result.returns.span(), format!("Invalid return mode. Allowed modes are: {ALLOWED_RETURN_MODES:?}"), )); } Ok(result) } fn options(&self) -> TokenStream { let returns = &self.returns; let backdate_ident = if self.has_no_eq_attr { syn::Ident::new("no_backdate", Span::call_site()) } else { syn::Ident::new("backdate", Span::call_site()) }; let default_ident = if self.has_default_attr { syn::Ident::new("default", Span::call_site()) } else { syn::Ident::new("required", Span::call_site()) }; quote!((#returns, #backdate_ident, #default_ident)) } } salsa-macros-0.23.0/src/supertype.rs000064400000000000000000000071111046102023000154700ustar 00000000000000use proc_macro2::TokenStream; use crate::token_stream_with_error; /// The implementation of the `supertype` macro. /// /// For an entity enum `Foo` with variants `Variant1, ..., VariantN`, we generate /// mappings between the variants and their corresponding supertypes. pub(crate) fn supertype(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let enum_item = parse_macro_input!(input as syn::ItemEnum); match enum_impl(enum_item) { Ok(v) => v.into(), Err(e) => token_stream_with_error(input, e), } } fn enum_impl(enum_item: syn::ItemEnum) -> syn::Result { let enum_name = enum_item.ident.clone(); let mut variant_names = Vec::new(); let mut variant_types = Vec::new(); if enum_item.variants.is_empty() { return Err(syn::Error::new( enum_item.enum_token.span, "empty enums are not permitted", )); } for variant in &enum_item.variants { let valid = match &variant.fields { syn::Fields::Unnamed(fields) => { variant_names.push(variant.ident.clone()); variant_types.push(fields.unnamed[0].ty.clone()); fields.unnamed.len() == 1 } syn::Fields::Unit | syn::Fields::Named(_) => false, }; if !valid { return Err(syn::Error::new( variant.ident.span(), "the only form allowed is `Variant(SalsaStruct)`", )); } } let (impl_generics, type_generics, where_clause) = enum_item.generics.split_for_impl(); let as_id = quote! { impl #impl_generics zalsa::AsId for #enum_name #type_generics #where_clause { #[inline] fn as_id(&self) -> zalsa::Id { match self { #( Self::#variant_names(__v) => zalsa::AsId::as_id(__v), )* } } } }; let from_id = quote! { impl #impl_generics zalsa::FromIdWithDb for #enum_name #type_generics #where_clause { #[inline] fn from_id(__id: zalsa::Id, zalsa: &zalsa::Zalsa) -> Self { let __type_id = zalsa.lookup_page_type_id(__id); ::cast(__id, __type_id).expect("invalid enum variant") } } }; let salsa_struct_in_db = quote! { impl #impl_generics zalsa::SalsaStructInDb for #enum_name #type_generics #where_clause { type MemoIngredientMap = zalsa::MemoIngredientIndices; #[inline] fn lookup_or_create_ingredient_index(__zalsa: &zalsa::Zalsa) -> zalsa::IngredientIndices { zalsa::IngredientIndices::merge([ #( <#variant_types as zalsa::SalsaStructInDb>::lookup_or_create_ingredient_index(__zalsa) ),* ]) } #[inline] fn cast(id: zalsa::Id, type_id: ::core::any::TypeId) -> Option { #( // Subtle: the ingredient can be missing, but in this case the id cannot come // from it - because it wasn't initialized yet. if let Some(result) = <#variant_types as zalsa::SalsaStructInDb>::cast(id, type_id) { Some(Self::#variant_names(result)) } else )* { None } } } }; let all_impls = quote! { const _: () = { use salsa::plumbing as zalsa; #as_id #from_id #salsa_struct_in_db }; }; Ok(all_impls) } salsa-macros-0.23.0/src/tracked.rs000064400000000000000000000014201046102023000150420ustar 00000000000000use syn::spanned::Spanned; use syn::Item; use crate::token_stream_with_error; pub(crate) fn tracked( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let item = parse_macro_input!(input as Item); let res = match item { syn::Item::Struct(item) => crate::tracked_struct::tracked_struct(args, item), syn::Item::Fn(item) => crate::tracked_fn::tracked_fn(args, item), syn::Item::Impl(item) => crate::tracked_impl::tracked_impl(args, item), _ => Err(syn::Error::new( item.span(), "tracked can only be applied to structs, functions, and impls", )), }; match res { Ok(s) => s.into(), Err(err) => token_stream_with_error(input, err), } } salsa-macros-0.23.0/src/tracked_fn.rs000064400000000000000000000354111046102023000155340ustar 00000000000000use proc_macro2::{Literal, Span, TokenStream}; use quote::ToTokens; use syn::spanned::Spanned; use syn::{Ident, ItemFn}; use crate::hygiene::Hygiene; use crate::options::Options; use crate::{db_lifetime, fn_util}; // Source: // // #[salsa::db] // pub struct Database { // storage: salsa::Storage, // } pub(crate) fn tracked_fn(args: proc_macro::TokenStream, item: ItemFn) -> syn::Result { let hygiene = Hygiene::from2(&item); let args: FnArgs = syn::parse(args)?; let db_macro = Macro { hygiene, args }; db_macro.try_fn(item) } pub type FnArgs = Options; pub struct TrackedFn; impl crate::options::AllowedOptions for TrackedFn { const RETURNS: bool = true; const SPECIFY: bool = true; const NO_EQ: bool = true; const DEBUG: bool = false; const NO_LIFETIME: bool = false; const NON_UPDATE_RETURN_TYPE: bool = true; const SINGLETON: bool = false; const DATA: bool = false; const DB: bool = false; const CYCLE_FN: bool = true; const CYCLE_INITIAL: bool = true; const CYCLE_RESULT: bool = true; const LRU: bool = true; const CONSTRUCTOR_NAME: bool = false; const ID: bool = false; const REVISIONS: bool = false; const HEAP_SIZE: bool = true; const SELF_TY: bool = true; } struct Macro { hygiene: Hygiene, args: FnArgs, } struct ValidFn<'item> { db_ident: &'item syn::Ident, db_path: &'item syn::Path, } const ALLOWED_RETURN_MODES: &[&str] = &["copy", "clone", "ref", "deref", "as_ref", "as_deref"]; #[allow(non_snake_case)] impl Macro { fn try_fn(&self, item: syn::ItemFn) -> syn::Result { let ValidFn { db_ident, db_path } = self.validity_check(&item)?; let attrs = &item.attrs; let fn_name = &item.sig.ident; let vis = &item.vis; let db_lt = db_lifetime::db_lifetime(&item.sig.generics); let input_ids = self.input_ids(&item); let input_tys = self.input_tys(&item)?; let interned_input_tys = input_tys.iter().map(|&ty| { let mut ty = ty.clone(); syn::visit_mut::visit_type_mut( &mut ToDbLifetimeVisitor { db_lifetime: db_lt.clone(), }, &mut ty, ); ty }); let output_ty = self.output_ty(&db_lt, &item)?; let (cycle_recovery_fn, cycle_recovery_initial, cycle_recovery_strategy) = self.cycle_recovery()?; let is_specifiable = self.args.specify.is_some(); let requires_update = self.args.non_update_return_type.is_none(); let heap_size_fn = self.args.heap_size_fn.iter(); let eq = if let Some(token) = &self.args.no_eq { if self.args.cycle_fn.is_some() { return Err(syn::Error::new_spanned( token, "the `no_eq` option cannot be used with `cycle_fn`", )); } quote!(false) } else { quote_spanned!(output_ty.span() => old_value == new_value ) }; // we need to generate the entire function here // as the locals (parameters) will have def site hygiene otherwise // if emitted in the decl macro let eq = quote! { fn values_equal<#db_lt>( old_value: &Self::Output<#db_lt>, new_value: &Self::Output<#db_lt>, ) -> bool { #eq } }; let mut inner_fn = item.clone(); inner_fn.vis = syn::Visibility::Inherited; inner_fn.sig.ident = self.hygiene.ident("inner"); let zalsa = self.hygiene.ident("zalsa"); let Configuration = self.hygiene.ident("Configuration"); let InternedData = self.hygiene.ident("InternedData"); let FN_CACHE = self.hygiene.ident("FN_CACHE"); let INTERN_CACHE = self.hygiene.ident("INTERN_CACHE"); let inner = &inner_fn.sig.ident; let function_type = function_type(&item); if is_specifiable { match function_type { FunctionType::Constant | FunctionType::RequiresInterning => { return Err(syn::Error::new_spanned( self.args.specify.as_ref().unwrap(), "only functions with a single salsa struct as their input can be specified", )) } FunctionType::SalsaStruct => {} } } if let (Some(_), Some(token)) = (&self.args.lru, &self.args.specify) { return Err(syn::Error::new_spanned( token, "the `specify` and `lru` options cannot be used together", )); } let needs_interner = match function_type { FunctionType::Constant | FunctionType::RequiresInterning => true, FunctionType::SalsaStruct => false, }; let lru = Literal::usize_unsuffixed(self.args.lru.unwrap_or(0)); let return_mode = self .args .returns .clone() .unwrap_or(Ident::new("clone", Span::call_site())); // Validate return mode if !ALLOWED_RETURN_MODES .iter() .any(|mode| mode == &return_mode.to_string()) { return Err(syn::Error::new( return_mode.span(), format!("Invalid return mode. Allowed modes are: {ALLOWED_RETURN_MODES:?}"), )); } // The path expression is responsible for emitting the primary span in the diagnostic we // want, so by uniformly using `output_ty.span()` we ensure that the diagnostic is emitted // at the return type in the original input. // See the tests/compile-fail/tracked_fn_return_ref.rs test let maybe_update_path = quote_spanned! {output_ty.span() => UpdateDispatch::<#output_ty>::maybe_update }; let assert_return_type_is_update = if requires_update { quote! { #[allow(clippy::all, warnings)] fn _assert_return_type_is_update<#db_lt>() { use #zalsa::{UpdateFallback, UpdateDispatch}; #maybe_update_path; } } } else { quote! {} }; let self_ty = match &self.args.self_ty { Some(ty) => quote! { self_ty: #ty, }, None => quote! {}, }; Ok(crate::debug::dump_tokens( fn_name, quote![salsa::plumbing::setup_tracked_fn! { attrs: [#(#attrs),*], vis: #vis, fn_name: #fn_name, db_lt: #db_lt, Db: #db_path, db: #db_ident, input_ids: [#(#input_ids),*], input_tys: [#(#input_tys),*], interned_input_tys: [#(#interned_input_tys),*], output_ty: #output_ty, inner_fn: { #inner_fn }, cycle_recovery_fn: #cycle_recovery_fn, cycle_recovery_initial: #cycle_recovery_initial, cycle_recovery_strategy: #cycle_recovery_strategy, is_specifiable: #is_specifiable, values_equal: {#eq}, needs_interner: #needs_interner, heap_size_fn: #(#heap_size_fn)*, lru: #lru, return_mode: #return_mode, assert_return_type_is_update: { #assert_return_type_is_update }, #self_ty unused_names: [ #zalsa, #Configuration, #InternedData, #FN_CACHE, #INTERN_CACHE, #inner, ] }], )) } fn validity_check<'item>(&self, item: &'item syn::ItemFn) -> syn::Result> { db_lifetime::require_optional_db_lifetime(&item.sig.generics)?; if item.sig.inputs.is_empty() { return Err(syn::Error::new_spanned( &item.sig.ident, "tracked functions must have at least a database argument", )); } let (db_ident, db_path) = check_db_argument(&item.sig.inputs[0], item.sig.generics.lifetimes().next())?; Ok(ValidFn { db_ident, db_path }) } fn cycle_recovery(&self) -> syn::Result<(TokenStream, TokenStream, TokenStream)> { // TODO should we ask the user to specify a struct that impls a trait with two methods, // rather than asking for two methods separately? match ( &self.args.cycle_fn, &self.args.cycle_initial, &self.args.cycle_result, ) { (Some(cycle_fn), Some(cycle_initial), None) => Ok(( quote!((#cycle_fn)), quote!((#cycle_initial)), quote!(Fixpoint), )), (None, None, None) => Ok(( quote!((salsa::plumbing::unexpected_cycle_recovery!)), quote!((salsa::plumbing::unexpected_cycle_initial!)), quote!(Panic), )), (Some(_), None, None) => Err(syn::Error::new_spanned( self.args.cycle_fn.as_ref().unwrap(), "must provide `cycle_initial` along with `cycle_fn`", )), (None, Some(_), None) => Err(syn::Error::new_spanned( self.args.cycle_initial.as_ref().unwrap(), "must provide `cycle_fn` along with `cycle_initial`", )), (None, None, Some(cycle_result)) => Ok(( quote!((salsa::plumbing::unexpected_cycle_recovery!)), quote!((#cycle_result)), quote!(FallbackImmediate), )), (_, _, Some(_)) => Err(syn::Error::new_spanned( self.args.cycle_initial.as_ref().unwrap(), "must provide either `cycle_result` or `cycle_fn` & `cycle_initial`, not both", )), } } fn input_ids(&self, item: &ItemFn) -> Vec { fn_util::input_ids(&self.hygiene, &item.sig, 1) } fn input_tys<'syn>(&self, item: &'syn ItemFn) -> syn::Result> { fn_util::input_tys(&item.sig, 1) } fn output_ty(&self, db_lt: &syn::Lifetime, item: &syn::ItemFn) -> syn::Result { fn_util::output_ty(Some(db_lt), &item.sig) } } struct ToDbLifetimeVisitor { db_lifetime: syn::Lifetime, } impl syn::visit_mut::VisitMut for ToDbLifetimeVisitor { fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) { i.clone_from(&self.db_lifetime); } } #[derive(Debug, PartialEq, Eq, Hash)] enum FunctionType { Constant, SalsaStruct, RequiresInterning, } fn function_type(item_fn: &syn::ItemFn) -> FunctionType { match item_fn.sig.inputs.len() { 0 => unreachable!( "functions have been checked to have at least a database argument by this point" ), 1 => FunctionType::Constant, 2 => FunctionType::SalsaStruct, _ => FunctionType::RequiresInterning, } } pub fn check_db_argument<'arg>( fn_arg: &'arg syn::FnArg, explicit_lt: Option<&'arg syn::LifetimeParam>, ) -> syn::Result<(&'arg syn::Ident, &'arg syn::Path)> { match fn_arg { syn::FnArg::Receiver(_) => { // If we see `&self` where a database was expected, that indicates // that `#[tracked]` was applied to a method. Err(syn::Error::new_spanned( fn_arg, "#[salsa::tracked] must also be applied to the impl block for tracked methods", )) } syn::FnArg::Typed(typed) => { let syn::Pat::Ident(db_pat_ident) = &*typed.pat else { return Err(syn::Error::new_spanned( &typed.pat, "database parameter must have a simple name", )); }; let syn::PatIdent { attrs, by_ref, mutability, ident: db_ident, subpat, } = db_pat_ident; if !attrs.is_empty() { return Err(syn::Error::new_spanned( db_pat_ident, "database parameter cannot have attributes", )); } if by_ref.is_some() { return Err(syn::Error::new_spanned( by_ref, "database parameter cannot be borrowed", )); } if mutability.is_some() { return Err(syn::Error::new_spanned( mutability, "database parameter cannot be mutable", )); } if let Some((at, _)) = subpat { return Err(syn::Error::new_spanned( at, "database parameter cannot have a subpattern", )); } let tykind_error_msg = "must have type `&dyn Db`, where `Db` is some Salsa Database trait"; let syn::Type::Reference(ref_type) = &*typed.ty else { return Err(syn::Error::new(typed.ty.span(), tykind_error_msg)); }; if let Some(lt) = explicit_lt { if ref_type.lifetime.is_none() { return Err(syn::Error::new_spanned( ref_type.and_token, format!("must have a `{}` lifetime", lt.lifetime.to_token_stream()), )); } } let extract_db_path = || -> Result<&'arg syn::Path, Span> { if let Some(m) = &ref_type.mutability { return Err(m.span()); } let syn::Type::TraitObject(d) = &*ref_type.elem else { return Err(ref_type.span()); }; if d.bounds.len() != 1 { return Err(d.span()); } let syn::TypeParamBound::Trait(syn::TraitBound { paren_token, modifier, lifetimes, path, }) = &d.bounds[0] else { return Err(d.span()); }; if let Some(p) = paren_token { return Err(p.span.open()); } let syn::TraitBoundModifier::None = modifier else { return Err(d.span()); }; if let Some(lt) = lifetimes { return Err(lt.span()); } Ok(path) }; let db_path = extract_db_path().map_err(|span| syn::Error::new(span, tykind_error_msg))?; Ok((db_ident, db_path)) } } } salsa-macros-0.23.0/src/tracked_impl.rs000064400000000000000000000303341046102023000160710ustar 00000000000000use std::collections::HashSet; use proc_macro2::TokenStream; use quote::ToTokens; use syn::parse::Nothing; use syn::visit_mut::VisitMut; use crate::hygiene::Hygiene; use crate::tracked_fn::FnArgs; use crate::xform::ChangeSelfPath; pub(crate) fn tracked_impl( args: proc_macro::TokenStream, item: syn::ItemImpl, ) -> syn::Result { let hygiene = Hygiene::from2(&item); let _: Nothing = syn::parse(args)?; let m = Macro { hygiene }; let generated = m.try_generate(item)?; Ok(generated) } struct Macro { hygiene: Hygiene, } struct AssociatedFunctionArguments<'syn> { self_token: Option<&'syn syn::token::SelfValue>, db_ty: &'syn syn::Type, db_ident: &'syn syn::Ident, db_lt: Option<&'syn syn::Lifetime>, input_ids: Vec, input_tys: Vec<&'syn syn::Type>, output_ty: syn::Type, } impl Macro { fn try_generate(&self, mut impl_item: syn::ItemImpl) -> syn::Result { let mut member_items = std::mem::take(&mut impl_item.items); let member_idents: HashSet<_> = member_items .iter() .filter_map(|item| match item { syn::ImplItem::Const(it) => Some(it.ident.clone()), syn::ImplItem::Fn(it) => Some(it.sig.ident.clone()), syn::ImplItem::Type(it) => Some(it.ident.clone()), syn::ImplItem::Macro(_) => None, syn::ImplItem::Verbatim(_) => None, _ => None, }) .collect(); for member_item in &mut member_items { self.modify_member(&impl_item, member_item, &member_idents)?; } impl_item.items = member_items; Ok(crate::debug::dump_tokens( format!("impl {:?}", impl_item.self_ty), impl_item.into_token_stream(), )) } #[allow(non_snake_case)] fn modify_member( &self, impl_item: &syn::ItemImpl, member_item: &mut syn::ImplItem, member_idents: &HashSet, ) -> syn::Result<()> { let syn::ImplItem::Fn(fn_item) = member_item else { return Ok(()); }; let self_ty = &*impl_item.self_ty; let Some(tracked_attr_index) = fn_item.attrs.iter().position(|a| self.is_tracked_attr(a)) else { return Ok(()); }; let trait_ = match &impl_item.trait_ { Some((None, path, _)) => Some((path, member_idents)), _ => None, }; let mut change = ChangeSelfPath::new(self_ty, trait_); change.visit_impl_item_fn_mut(fn_item); let mut salsa_tracked_attr = fn_item.attrs.remove(tracked_attr_index); let mut args: FnArgs = match &salsa_tracked_attr.meta { syn::Meta::Path(..) => Default::default(), _ => salsa_tracked_attr.parse_args()?, }; if args.self_ty.is_none() { // If the user did not specify a self_ty, we use the impl's self_ty args.self_ty = Some(self_ty.clone()); } salsa_tracked_attr.meta = syn::Meta::List(syn::MetaList { path: salsa_tracked_attr.path().clone(), delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()), tokens: quote! {#args}, }); let InnerTrait = self.hygiene.ident("InnerTrait"); let inner_fn_name = self.hygiene.ident(&fn_item.sig.ident.to_string()); let AssociatedFunctionArguments { self_token, db_ty, db_ident, db_lt, input_ids, input_tys, output_ty, } = self.validity_check(impl_item, fn_item)?; let mut inner_fn = fn_item.clone(); inner_fn.vis = syn::Visibility::Inherited; inner_fn.sig.ident = inner_fn_name.clone(); // Construct the body of the method or associated function let block = if let Some(self_token) = self_token { parse_quote!({ salsa::plumbing::setup_tracked_method_body! { salsa_tracked_attr: #salsa_tracked_attr, self: #self_token, self_ty: #self_ty, db_lt: #db_lt, db: #db_ident, db_ty: (#db_ty), input_ids: [#(#input_ids),*], input_tys: [#(#input_tys),*], output_ty: #output_ty, inner_fn_name: #inner_fn_name, inner_fn: #inner_fn, // 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: [ #InnerTrait, ] } }) } else { parse_quote!({ salsa::plumbing::setup_tracked_assoc_fn_body! { salsa_tracked_attr: #salsa_tracked_attr, self_ty: #self_ty, db_lt: #db_lt, db: #db_ident, db_ty: (#db_ty), input_ids: [#(#input_ids),*], input_tys: [#(#input_tys),*], output_ty: #output_ty, inner_fn_name: #inner_fn_name, inner_fn: #inner_fn, // 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: [ #InnerTrait, ] } }) }; // Update the method that will actually appear in the impl to have the new body // and its true return type let db_lt = db_lt.cloned(); self.update_return_type(&mut fn_item.sig, &args, &db_lt)?; fn_item.block = block; Ok(()) } fn validity_check<'syn>( &self, impl_item: &'syn syn::ItemImpl, fn_item: &'syn syn::ImplItemFn, ) -> syn::Result> { let db_lt = self.extract_db_lifetime(impl_item, fn_item)?; let is_method = matches!(&fn_item.sig.inputs[0], syn::FnArg::Receiver(_)); let (self_token, db_input_index, skipped_inputs) = if is_method { (Some(self.check_self_argument(fn_item)?), 1, 2) } else { (None, 0, 1) }; let (db_ident, db_ty) = self.check_db_argument(&fn_item.sig.inputs[db_input_index])?; let input_ids: Vec = crate::fn_util::input_ids(&self.hygiene, &fn_item.sig, skipped_inputs); let input_tys = crate::fn_util::input_tys(&fn_item.sig, skipped_inputs)?; let output_ty = crate::fn_util::output_ty(db_lt, &fn_item.sig)?; Ok(AssociatedFunctionArguments { self_token, db_ident, db_lt, db_ty, input_ids, input_tys, output_ty, }) } fn is_tracked_attr(&self, attr: &syn::Attribute) -> bool { if attr.path().segments.len() != 2 { return false; } let seg0 = &attr.path().segments[0]; let seg1 = &attr.path().segments[1]; seg0.ident == "salsa" && seg1.ident == "tracked" && seg0.arguments.is_empty() && seg1.arguments.is_empty() } fn extract_db_lifetime<'syn>( &self, impl_item: &'syn syn::ItemImpl, fn_item: &'syn syn::ImplItemFn, ) -> syn::Result> { // Either the impl XOR the fn can have generics, and it must be at most a lifetime let mut db_lt = None; for param in impl_item .generics .params .iter() .chain(fn_item.sig.generics.params.iter()) { match param { syn::GenericParam::Lifetime(lt) => { if db_lt.is_none() { if let Some(bound) = lt.bounds.iter().next() { return Err(syn::Error::new_spanned( bound, "lifetime parameters on tracked methods must not have bounds", )); } db_lt = Some(<.lifetime); } else { return Err(syn::Error::new_spanned( param, "tracked method already has a lifetime parameter in scope", )); } } _ => { return Err(syn::Error::new_spanned( param, "tracked methods cannot have non-lifetime generic parameters", )); } } } Ok(db_lt) } fn check_self_argument<'syn>( &self, fn_item: &'syn syn::ImplItemFn, ) -> syn::Result<&'syn syn::token::SelfValue> { if fn_item.sig.inputs.is_empty() { return Err(syn::Error::new_spanned( &fn_item.sig.ident, "tracked methods must have arguments", )); } let syn::FnArg::Receiver(syn::Receiver { attrs: _, self_token, reference, mutability: _, colon_token, ty: _, }) = &fn_item.sig.inputs[0] else { return Err(syn::Error::new_spanned( &fn_item.sig.inputs[0], "tracked methods must take a `self` argument", )); }; if let Some(colon_token) = colon_token { return Err(syn::Error::new_spanned( colon_token, "tracked method's `self` argument must not have an explicit type", )); } if let Some((and_token, _)) = reference { return Err(syn::Error::new_spanned( and_token, "tracked methods's first argument must be declared as `self`, not `&self` or `&mut self`", )); } Ok(self_token) } fn check_db_argument<'syn>( &self, input: &'syn syn::FnArg, ) -> syn::Result<(&'syn syn::Ident, &'syn syn::Type)> { let syn::FnArg::Typed(typed) = input else { return Err(syn::Error::new_spanned( input, "tracked methods must take a database parameter", )); }; let syn::Pat::Ident(db_pat_ident) = &*typed.pat else { return Err(syn::Error::new_spanned( &typed.pat, "database parameter must have a simple name", )); }; let db_ident = &db_pat_ident.ident; let db_ty = &*typed.ty; Ok((db_ident, db_ty)) } fn update_return_type( &self, sig: &mut syn::Signature, args: &FnArgs, db_lt: &Option, ) -> syn::Result<()> { if let Some(returns) = &args.returns { if let syn::ReturnType::Type(_, t) = &mut sig.output { if returns == "copy" || returns == "clone" { // leave as is } else if returns == "ref" { **t = parse_quote!(& #db_lt #t) } else if returns == "deref" { **t = parse_quote!(& #db_lt <#t as ::core::ops::Deref>::Target) } else if returns == "as_ref" { **t = parse_quote!(<#t as ::salsa::SalsaAsRef>::AsRef<#db_lt>) } else if returns == "as_deref" { **t = parse_quote!(<#t as ::salsa::SalsaAsDeref>::AsDeref<#db_lt>) } else { return Err(syn::Error::new_spanned( returns, format!("Unknown returns mode `{returns}`"), )); } } else { return Err(syn::Error::new_spanned( returns, "returns attribute requires explicit return type", )); }; } Ok(()) } } salsa-macros-0.23.0/src/tracked_struct.rs000064400000000000000000000153551046102023000164620ustar 00000000000000use proc_macro2::TokenStream; use syn::spanned::Spanned; use crate::db_lifetime; use crate::hygiene::Hygiene; use crate::options::Options; use crate::salsa_struct::{SalsaStruct, SalsaStructAllowedOptions}; /// 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 tracked_struct( args: proc_macro::TokenStream, struct_item: syn::ItemStruct, ) -> syn::Result { let hygiene = Hygiene::from2(&struct_item); let m = Macro { hygiene, args: syn::parse(args)?, struct_item, }; m.try_macro() } type TrackedArgs = Options; struct TrackedStruct; impl crate::options::AllowedOptions for TrackedStruct { const RETURNS: bool = false; const SPECIFY: bool = false; const NO_EQ: bool = false; const DEBUG: bool = true; const NO_LIFETIME: bool = false; const NON_UPDATE_RETURN_TYPE: bool = false; const SINGLETON: bool = true; const DATA: bool = true; const DB: bool = false; const CYCLE_FN: bool = false; const CYCLE_INITIAL: bool = false; const CYCLE_RESULT: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; const ID: bool = false; const REVISIONS: bool = false; const HEAP_SIZE: bool = false; const SELF_TY: bool = false; } impl SalsaStructAllowedOptions for TrackedStruct { const KIND: &'static str = "tracked"; const ALLOW_MAYBE_UPDATE: bool = true; const ALLOW_TRACKED: bool = true; const HAS_LIFETIME: bool = true; const ELIDABLE_LIFETIME: bool = false; const ALLOW_DEFAULT: bool = false; } struct Macro { hygiene: Hygiene, args: TrackedArgs, 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 zalsa = self.hygiene.ident("zalsa"); 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 tracked_ids = salsa_struct.tracked_ids(); let tracked_vis = salsa_struct.tracked_vis(); let untracked_vis = salsa_struct.untracked_vis(); let tracked_getter_ids = salsa_struct.tracked_getter_ids(); let untracked_getter_ids = salsa_struct.untracked_getter_ids(); let field_indices = salsa_struct.field_indices(); let absolute_tracked_indices = salsa_struct.tracked_field_indices(); let relative_tracked_indices = (0..absolute_tracked_indices.len()).collect::>(); let absolute_untracked_indices = salsa_struct.untracked_field_indices(); let tracked_options = salsa_struct.tracked_options(); let untracked_options = salsa_struct.untracked_options(); let field_tys = salsa_struct.field_tys(); let tracked_tys = salsa_struct.tracked_tys(); let untracked_tys = salsa_struct.untracked_tys(); let tracked_field_unused_attrs = salsa_struct.tracked_field_attrs(); let untracked_field_unused_attrs = salsa_struct.untracked_field_attrs(); let tracked_maybe_update = salsa_struct.tracked_fields_iter().map(|(_, field)| { let field_ty = &field.field.ty; if let Some((with_token, maybe_update)) = &field.maybe_update_attr { quote_spanned! { with_token.span() => ({ let maybe_update: unsafe fn(*mut #field_ty, #field_ty) -> bool = #maybe_update; maybe_update }) } } else { quote! {(#zalsa::UpdateDispatch::<#field_ty>::maybe_update)} } }); let untracked_maybe_update = salsa_struct.untracked_fields_iter().map(|(_, field)| { let field_ty = &field.field.ty; if let Some((with_token, maybe_update)) = &field.maybe_update_attr { quote_spanned! { with_token.span() => ({ let maybe_update: unsafe fn(*mut #field_ty, #field_ty) -> bool = #maybe_update; maybe_update }) } } else { quote! {(#zalsa::UpdateDispatch::<#field_ty>::maybe_update)} } }); let num_tracked_fields = salsa_struct.num_tracked_fields(); let generate_debug_impl = salsa_struct.generate_debug_impl(); 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"); let Revision = self.hygiene.ident("Revision"); Ok(crate::debug::dump_tokens( struct_ident, quote! { salsa::plumbing::setup_tracked_struct!( attrs: [#(#attrs),*], vis: #vis, Struct: #struct_ident, db_lt: #db_lt, new_fn: #new_fn, field_ids: [#(#field_ids),*], tracked_ids: [#(#tracked_ids),*], tracked_getters: [#(#tracked_vis #tracked_getter_ids),*], untracked_getters: [#(#untracked_vis #untracked_getter_ids),*], field_tys: [#(#field_tys),*], tracked_tys: [#(#tracked_tys),*], untracked_tys: [#(#untracked_tys),*], field_indices: [#(#field_indices),*], absolute_tracked_indices: [#(#absolute_tracked_indices),*], relative_tracked_indices: [#(#relative_tracked_indices),*], absolute_untracked_indices: [#(#absolute_untracked_indices),*], tracked_maybe_updates: [#(#tracked_maybe_update),*], untracked_maybe_updates: [#(#untracked_maybe_update),*], tracked_options: [#(#tracked_options),*], untracked_options: [#(#untracked_options),*], tracked_field_attrs: [#([#(#tracked_field_unused_attrs),*]),*], untracked_field_attrs: [#([#(#untracked_field_unused_attrs),*]),*], num_tracked_fields: #num_tracked_fields, generate_debug_impl: #generate_debug_impl, unused_names: [ #zalsa, #zalsa_struct, #Configuration, #CACHE, #Db, #Revision, ] ); }, )) } } salsa-macros-0.23.0/src/update.rs000064400000000000000000000126021046102023000147130ustar 00000000000000use proc_macro2::{Literal, Span, TokenStream}; use syn::{parenthesized, parse::ParseStream, spanned::Spanned, Token}; use synstructure::BindStyle; use crate::{hygiene::Hygiene, kw}; pub(crate) fn update_derive(input: syn::DeriveInput) -> syn::Result { let hygiene = Hygiene::from2(&input); if let syn::Data::Union(u) = &input.data { return Err(syn::Error::new_spanned( u.union_token, "`derive(Update)` does not support `union`", )); } let mut structure = synstructure::Structure::new(&input); for v in structure.variants_mut() { v.bind_with(|_| BindStyle::Move); } let old_pointer = hygiene.ident("old_pointer"); let new_value = hygiene.ident("new_value"); let fields: TokenStream = structure .variants() .iter() .map(|variant| { let err = variant .ast() .attrs .iter() .filter(|attr| attr.path().is_ident("update")) .map(|attr| { syn::Error::new( attr.path().span(), "unexpected attribute `#[update]` on variant", ) }) .reduce(|mut acc, err| { acc.combine(err); acc }); if let Some(err) = err { return Err(err); } let variant_pat = variant.pat(); // First check that the `new_value` has same variant. // Extract its fields and convert to a tuple. let make_tuple = variant .bindings() .iter() .fold(quote!(), |tokens, binding| quote!(#tokens #binding,)); let make_new_value = quote! { let #new_value = if let #variant_pat = #new_value { (#make_tuple) } else { *#old_pointer = #new_value; return true; }; }; // For each field, invoke `maybe_update` recursively to update its value. // Or the results together (using `|`, not `||`, to avoid shortcircuiting) // to get the final return value. let mut update_fields = quote!(false); for (index, binding) in variant.bindings().iter().enumerate() { let mut attrs = binding .ast() .attrs .iter() .filter(|attr| attr.path().is_ident("update")); let attr = attrs.next(); if let Some(attr) = attrs.next() { return Err(syn::Error::new( attr.path().span(), "multiple #[update(with)] attributes on field", )); } let field_ty = &binding.ast().ty; let field_index = Literal::usize_unsuffixed(index); let (maybe_update, unsafe_token) = match attr { Some(attr) => { attr.parse_args_with(|parser: ParseStream| { let mut content; let unsafe_token = parser.parse::()?; parenthesized!(content in parser); let with_token = content.parse::()?; parenthesized!(content in content); let expr = content.parse::()?; Ok(( quote_spanned! { with_token.span() => ({ let maybe_update: unsafe fn(*mut #field_ty, #field_ty) -> bool = #expr; maybe_update }) }, unsafe_token, )) })? } None => { ( quote!( salsa::plumbing::UpdateDispatch::<#field_ty>::maybe_update ), Token![unsafe](Span::call_site()), ) } }; let update_field = quote! { #maybe_update( #binding, #new_value.#field_index, ) }; update_fields = quote! { #update_fields | #unsafe_token { #update_field } }; } Ok(quote!( #variant_pat => { #make_new_value #update_fields } )) }) .collect::>()?; let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let tokens = quote! { #[allow(clippy::all)] #[automatically_derived] unsafe impl #impl_generics salsa::Update for #ident #ty_generics #where_clause { unsafe fn maybe_update(#old_pointer: *mut Self, #new_value: Self) -> bool { use ::salsa::plumbing::UpdateFallback as _; let #old_pointer = unsafe { &mut *#old_pointer }; match #old_pointer { #fields } } } }; Ok(crate::debug::dump_tokens(&input.ident, tokens)) } salsa-macros-0.23.0/src/xform.rs000064400000000000000000000110471046102023000145660ustar 00000000000000use std::collections::HashSet; use quote::ToTokens; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; pub(crate) struct ChangeLt<'a> { from: Option<&'a str>, to: String, } impl ChangeLt<'_> { pub fn elided_to(db_lt: &syn::Lifetime) -> Self { ChangeLt { from: Some("_"), to: db_lt.ident.to_string(), } } pub fn in_type(mut self, ty: &syn::Type) -> syn::Type { let mut ty = ty.clone(); self.visit_type_mut(&mut ty); ty } } impl syn::visit_mut::VisitMut for ChangeLt<'_> { fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) { if self.from.map(|f| i.ident == f).unwrap_or(true) { i.ident = syn::Ident::new(&self.to, i.ident.span()); } } } pub(crate) struct ChangeSelfPath<'a> { self_ty: &'a syn::Type, trait_: Option<(&'a syn::Path, &'a HashSet)>, } impl ChangeSelfPath<'_> { pub fn new<'a>( self_ty: &'a syn::Type, trait_: Option<(&'a syn::Path, &'a HashSet)>, ) -> ChangeSelfPath<'a> { ChangeSelfPath { self_ty, trait_ } } } impl syn::visit_mut::VisitMut for ChangeSelfPath<'_> { fn visit_type_mut(&mut self, i: &mut syn::Type) { if let syn::Type::Path(syn::TypePath { qself: None, path }) = i { if path.segments.len() == 1 && path.segments.first().is_some_and(|s| s.ident == "Self") { let span = path.segments.first().unwrap().span(); *i = respan(self.self_ty, span); } } syn::visit_mut::visit_type_mut(self, i); } fn visit_type_path_mut(&mut self, i: &mut syn::TypePath) { // `` cases are handled in `visit_type_mut` if i.qself.is_some() { syn::visit_mut::visit_type_path_mut(self, i); return; } // A single path `Self` case is handled in `visit_type_mut` if i.path.segments.first().is_some_and(|s| s.ident == "Self") && i.path.segments.len() > 1 { let span = i.path.segments.first().unwrap().span(); let ty = Box::new(respan::(self.self_ty, span)); let lt_token = syn::Token![<](span); let gt_token = syn::Token![>](span); match self.trait_ { // If the next segment's ident is a trait member, replace `Self::` with // `::` Some((trait_, member_idents)) if member_idents.contains(&i.path.segments.iter().nth(1).unwrap().ident) => { let qself = syn::QSelf { lt_token, ty, position: trait_.segments.len(), as_token: Some(syn::Token![as](span)), gt_token, }; i.qself = Some(qself); i.path.segments = Punctuated::from_iter( trait_ .segments .iter() .chain(i.path.segments.iter().skip(1)) .cloned(), ); } // Replace `Self::` with `::` otherwise _ => { let qself = syn::QSelf { lt_token, ty, position: 0, as_token: None, gt_token, }; i.qself = Some(qself); i.path.segments = Punctuated::from_iter(i.path.segments.iter().skip(1).cloned()); } } } syn::visit_mut::visit_type_path_mut(self, i); } } fn respan(t: &T, span: proc_macro2::Span) -> T where T: ToTokens + Spanned + syn::parse::Parse, { let tokens = t.to_token_stream(); let respanned = respan_tokenstream(tokens, span); syn::parse2(respanned).unwrap() } fn respan_tokenstream( stream: proc_macro2::TokenStream, span: proc_macro2::Span, ) -> proc_macro2::TokenStream { stream .into_iter() .map(|token| respan_token(token, span)) .collect() } fn respan_token( mut token: proc_macro2::TokenTree, span: proc_macro2::Span, ) -> proc_macro2::TokenTree { if let proc_macro2::TokenTree::Group(g) = &mut token { *g = proc_macro2::Group::new(g.delimiter(), respan_tokenstream(g.stream(), span)); } token.set_span(span); token }