diff --git a/Cargo.lock b/Cargo.lock index f090e1f..b005172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1383,7 +1383,9 @@ name = "synthizer_macros_internal" version = "0.1.0" dependencies = [ "darling", + "ident_case", "proc-macro-error", + "proc-macro2", "quote", "syn 2.0.38", ] diff --git a/Cargo.toml b/Cargo.toml index 74260cb..154e7ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ derive_more = "0.99.17" enum_dispatch = "0.3.12" env_logger = "0.10.0" eye_dropper = { path = "crates/eye_dropper" } +ident_case = "1.0.1" im = "15.1.0" itertools = "0.10.5" lazy_static = "1.4.0" diff --git a/crates/synthizer_macros_internal/Cargo.toml b/crates/synthizer_macros_internal/Cargo.toml index f5cae64..4175954 100644 --- a/crates/synthizer_macros_internal/Cargo.toml +++ b/crates/synthizer_macros_internal/Cargo.toml @@ -10,6 +10,8 @@ proc-macro = true [dependencies] darling.workspace = true +ident_case.workspace = true +proc-macro2.workspace = true proc-macro-error.workspace = true quote.workspace = true syn.workspace = true diff --git a/crates/synthizer_macros_internal/src/lib.rs b/crates/synthizer_macros_internal/src/lib.rs index 40ae8be..0d5332e 100644 --- a/crates/synthizer_macros_internal/src/lib.rs +++ b/crates/synthizer_macros_internal/src/lib.rs @@ -5,3 +5,17 @@ //! lot of boilerplate abstraction, for example understanding the concept of a property and building node descriptors. //! Rather than type this tens of times and refactor them all tens of times per change, we have this crate to help us //! out. +//! +//! This crate contains dummy forwarders at the root to let us split it into files. See the individual modules for docs +//! on the macro. Procmacro limitations currently require that they be at the root. +mod property_slots_impl; +mod utils; + +#[proc_macro_attribute] +#[proc_macro_error::proc_macro_error] +pub fn property_slots( + attrs: proc_macro::TokenStream, + body: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + property_slots_impl::property_slots_impl(attrs.into(), body.into()).into() +} diff --git a/crates/synthizer_macros_internal/src/property_slots_impl.rs b/crates/synthizer_macros_internal/src/property_slots_impl.rs new file mode 100644 index 0000000..b562296 --- /dev/null +++ b/crates/synthizer_macros_internal/src/property_slots_impl.rs @@ -0,0 +1,138 @@ +//! A macro to handle PropertyCommandReceiver, which we apply to XXXSlots structs. This does two things: +//! +//! - Implements PropertyCommandReceiver. +//! - Creates a XXXProperties struct which is for the public API of a node. +//! +//! Input structs *must* be named with the Slots suffix, and will be sealed magically with this macro. See the structs +//! in this file for input options. +//! +//! This is an attribute macro and not a derive because it must seal the struct, and so must have the ability to move +//! the original definition. All struct-level options will go in the `property_slots` attribute (also the macro +//! invocation) and (later, when we implement options for such), field options will be `slot`. +//! +//! To document properties, document them on the input struct. The docs are then copied over to the output and (in +//! future) augmented with additional metadata such as ranges. +//! +//! It is strictly assumed that all fields of a slots struct are `PropertySlot`. +use darling::{FromDeriveInput, FromField}; +use syn::parse::Parse; +use syn::spanned::Spanned; + +// Note that we can get a DeriveInput from an attribute macro simply by ignoring the attribute token stream. + +#[derive(Debug, FromDeriveInput)] +#[darling(supports(struct_named))] +struct PropSlotsInput { + ident: syn::Ident, + data: darling::ast::Data<(), syn::Field>, +} + +pub(crate) fn property_slots_impl( + _attrs: proc_macro2::TokenStream, + body: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let syn_input: syn::DeriveInput = match syn::parse2(body.clone()) { + Ok(x) => x, + Err(e) => proc_macro_error::abort_call_site!(e), + }; + + let input = match PropSlotsInput::from_derive_input(&syn_input) { + Ok(x) => x, + Err(e) => { + return e.write_errors().into(); + } + }; + + if !input.ident.to_string().ends_with("Slots") { + proc_macro_error::abort!(input.ident, "Slot struct names must end with 'Slots'"); + } + + // The input to this macro is already a complete slots struct; sep 1 is to seal it. + let sealed_slots = crate::utils::seal(&input.ident, &body); + + // Step 2 is to generate a struct for the public API, of the form XxxProperties. We do this in three steps: + // + // 1. Figure out the identifier; + // 2. Figure out the declaration; + // 3. Figure out the PropertyCommandReceiver implementation. + + let mut public_name = input + .ident + .to_string() + .strip_suffix("Slots") + .expect("We validated that it ends with slots earlier") + .to_string(); + public_name.push_str("Props"); + + struct PropDef { + name: syn::Ident, + marker: syn::Type, + } + + let mut fields: Vec = vec![]; + + for f in input + .data + .clone() + .take_struct() + .expect("Darling should validate this is a struct") + .into_iter() + { + let name = f.ident.unwrap(); + let ty_path = match f.ty { + syn::Type::Path(ref p) => p.clone(), + _ => proc_macro_error::abort_call_site!( + "Found a non-struct field in this struct. Fields should always be 'Slot'" + ), + }; + + let last_seg = match ty_path.path.segments.last() { + Some(x) => x, + None => { + proc_macro_error::abort!( + f.ty.span(), + "This path has no segments; it should be of the form 'Slot'" + ); + } + }; + + // That last segment should have exactly one generic; that generic should be the path to a concrete marker type. + let args = match last_seg.arguments { + syn::PathArguments::AngleBracketed(ref a) => a.clone(), + _ => proc_macro_error::abort!( + last_seg.span(), + "This should have some generics on it, but does not" + ), + }; + + let args = args.args; + + if args.len() != 1 { + proc_macro_error::abort!(args.span(), "This should have exactly one generic argument"); + } + + let marker = match args[0] { + syn::GenericArgument::Type(ref t) => t.clone(), + _ => proc_macro_error::abort!(args[0].span(), "Thios should be a type"), + }; + + fields.push(PropDef { name, marker }); + } + + // Our generic lifetime is `'a`. + let public_field_decls = fields + .iter() + .map(|f| { + let name = &f.name; + let marker = &f.marker; + quote::quote!(pub(crate) #name: crate::properties::Property<'a, #marker>) + }) + .collect::>(); + + let pub_struct_decl = quote::quote!(pub struct #public_name<'a> { + #(#public_field_decls),* + }); + + + unreachable!() +} diff --git a/crates/synthizer_macros_internal/src/utils.rs b/crates/synthizer_macros_internal/src/utils.rs new file mode 100644 index 0000000..7c72782 --- /dev/null +++ b/crates/synthizer_macros_internal/src/utils.rs @@ -0,0 +1,61 @@ +/// Given an identifier an some code, wrap the code in a "sealed" module: +/// +/// ```IGNORE +/// mod xxx_sealed { +/// use super::*; +/// tokens here... +/// } +/// +/// pub(crate) use xxx_sealed::*; +/// ``` +/// +/// The sealed module name is derived from the identifier by converting it to snake case and adding _sealed. +pub(crate) fn seal(ident: &syn::Ident, tokens: &T) -> proc_macro2::TokenStream { + let cased = ident_case::RenameRule::SnakeCase.apply_to_field(ident.to_string()); + let mod_token: syn::Ident = syn::parse_quote!(#cased); + quote::quote!( + mod #mod_token { + use super::*; + + #tokens + } + + pub(crate) use sealed::*; + ) +} + +/// Builds a match statement like this: +/// +/// ```IGNORE +/// match index { +/// 0 => expr0, +/// 1 => expr1, +/// ... +/// _ => panic!("Index out of bounds"), +/// } +/// ``` +/// +/// This is used to allow turning user-supplied indices into references to fields on heterogeneous types, primarily +/// method calls, where we can't use arrays without overhead. This shows up in e.g. property slots, where propertis of +/// different types have different concrete representations. +fn build_indexing_match>( + index: syn::Expr, + items: I, +) -> syn::ExprMatch { + let arms = items + .enumerate() + .map(|(index, expr)| quote::quote!(#index => #expr)) + .collect::>(); + let arm_count = arms.len(); + let index_error = if arm_count > 0 { + format!("Index {{}} must not be over {}", arm_count - 1) + } else { + "Got index {}, but no indices are accepted here; this is expected to be dead code" + .to_string() + }; + + syn::parse_quote!(match #index { + #(#arms),* + _ => panic!(#index_error, #index), + }) +}