diff options
Diffstat (limited to 'mingling_macros')
| -rw-r--r-- | mingling_macros/src/attr.rs | 1 | ||||
| -rw-r--r-- | mingling_macros/src/attr/metadata.rs | 87 | ||||
| -rw-r--r-- | mingling_macros/src/func.rs | 1 | ||||
| -rw-r--r-- | mingling_macros/src/func/program_final_gen.rs | 35 | ||||
| -rw-r--r-- | mingling_macros/src/func/register_metadata.rs | 67 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 62 |
6 files changed, 252 insertions, 1 deletions
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs index 54fe2f1..59544a8 100644 --- a/mingling_macros/src/attr.rs +++ b/mingling_macros/src/attr.rs @@ -6,6 +6,7 @@ pub(crate) mod completion; #[cfg(feature = "clap")] pub(crate) mod dispatcher_clap; pub(crate) mod help; +pub(crate) mod metadata; pub(crate) mod mlint; #[cfg(feature = "extras")] pub(crate) mod program_setup; diff --git a/mingling_macros/src/attr/metadata.rs b/mingling_macros/src/attr/metadata.rs new file mode 100644 index 0000000..b96e319 --- /dev/null +++ b/mingling_macros/src/attr/metadata.rs @@ -0,0 +1,87 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{Attribute, ItemFn, ReturnType, TypePath, parse_macro_input}; + +/// Implements the `#[metadata(EntryVariant)]` attribute macro. +/// +/// It takes the enum variant ident to attach metadata to, and rewrites the +/// annotated function into: +/// - an `impl ::mingling::Metadata<ReturnType> for EntryVariant` that calls the +/// original function, +/// - a `::mingling::macros::register_metadata!(EntryVariant, ReturnType)` call, +/// - the preserved original function. +pub(crate) fn metadata_attr(attr: TokenStream, item: TokenStream) -> TokenStream { + let entry_variant = parse_macro_input!(attr as syn::Ident); + + let input_fn = parse_macro_input!(item as ItemFn); + + // The metadata type is the function's return type. + let metadata_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => match syn::parse2::<TypePath>(quote! { #ty }) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }, + ReturnType::Default => { + return syn::Error::new( + input_fn.sig.span(), + "#[metadata] requires the function to have an explicit return type", + ) + .to_compile_error() + .into(); + } + }; + + // Preserve the original return type exactly as written, so the original + // function signature is reproduced unchanged. + let original_return_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => quote! { #ty }, + ReturnType::Default => quote! { () }, + }; + + // Reject async metadata functions: `Metadata::init_metadata` is synchronous. + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Metadata function cannot be async") + .to_compile_error() + .into(); + } + + let fn_name = &input_fn.sig.ident; + let vis = &input_fn.vis; + let original_inputs = input_fn.sig.inputs.clone(); + let fn_body_stmts = &input_fn.block.stmts; + + // Function attributes, excluding the metadata attribute itself. + let fn_attrs: Vec<&Attribute> = input_fn + .attrs + .iter() + .filter(|attr| !attr.path().is_ident("metadata")) + .collect(); + + // A metadata provider is a zero-argument function. + if !original_inputs.is_empty() { + return syn::Error::new( + input_fn.sig.span(), + "#[metadata] function cannot take any parameters", + ) + .to_compile_error() + .into(); + } + + let expanded = quote! { + impl ::mingling::Metadata<#metadata_type> for #entry_variant { + fn init_metadata() -> #metadata_type { + #fn_name() + } + } + + ::mingling::macros::register_metadata!(#entry_variant, #metadata_type); + + #(#fn_attrs)* + #vis fn #fn_name(#original_inputs) -> #original_return_type { + #(#fn_body_stmts)* + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 9e0e15f..d566208 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -28,6 +28,7 @@ pub(crate) mod r_println; pub(crate) mod register_chain; pub(crate) mod register_dispatcher; pub(crate) mod register_help; +pub(crate) mod register_metadata; pub(crate) mod register_renderer; pub(crate) mod register_type; #[cfg(feature = "extras")] diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs index e8545f4..429e60c 100644 --- a/mingling_macros/src/func/program_final_gen.rs +++ b/mingling_macros/src/func/program_final_gen.rs @@ -8,6 +8,7 @@ use crate::COMPILE_TIME_DISPATCHERS; #[cfg(feature = "comp")] use crate::COMPLETIONS; use crate::HELP_REQUESTS; +use crate::METADATA; use crate::PACKED_TYPES; use crate::RENDERERS; use crate::RENDERERS_EXIST; @@ -269,6 +270,38 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) .collect(); + let metadata_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&METADATA) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let get_metadata_fn = if metadata_tokens.is_empty() { + quote! { + fn get_metadata<T: 'static>(_member_id: Self::Enum) -> Option<T> { + None + } + } + } else { + let metadata_arms = metadata_tokens.iter().map(|entry| { + quote! { + #entry + } + }); + quote! { + fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> { + let type_id = ::std::any::TypeId::of::<T>(); + let any = match member_id { + #(#metadata_arms)* + _ => None, + }; + any.and_then(|b| b.downcast::<T>().ok().map(|b| *b)) + } + } + }; + let num_variants = packed_types.len(); let repr_type = if u8::try_from(num_variants).is_ok() { quote! { u8 } @@ -313,6 +346,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { } #render_fn #do_chain_fn + #get_metadata_fn fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { match any.member_id() { #(#help_tokens)* @@ -356,6 +390,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { get_global_set(&RENDERERS).lock().unwrap().clear(); get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); + get_global_set(&METADATA).lock().unwrap().clear(); #[cfg(feature = "comp")] get_global_set(&COMPLETIONS).lock().unwrap().clear(); #[cfg(feature = "dispatch_tree")] diff --git a/mingling_macros/src/func/register_metadata.rs b/mingling_macros/src/func/register_metadata.rs new file mode 100644 index 0000000..a1f9965 --- /dev/null +++ b/mingling_macros/src/func/register_metadata.rs @@ -0,0 +1,67 @@ +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::TypePath; +use syn::spanned::Spanned; + +use crate::METADATA; +use crate::get_global_set; + +/// Parses and registers a metadata mapping of the form +/// `register_metadata!(EntryGreet, Description)`. +/// +/// Stores a match-arm-style string entry `Self::EntryGreet => { ... }` that is +/// later consumed by `program_final_gen!` to generate the `get_metadata` +/// method of `ProgramCollect`. +pub(crate) fn register_metadata_impl(input: TokenStream) -> TokenStream { + // Parse the input as a comma-separated list of type arguments. + let input_parsed = syn::parse_macro_input!( + input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated + ); + + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `EntryVariant, MetadataType`", + ) + .to_compile_error() + .into(); + } + + let entry_expr = &input_parsed[0]; + let metadata_expr = &input_parsed[1]; + + let entry_type = match syn::parse2::<TypePath>(entry_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + let metadata_type = match syn::parse2::<TypePath>(metadata_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let entry_str = build_metadata_entry(&entry_type, &metadata_type).to_string(); + + get_global_set(&METADATA).lock().unwrap().insert(entry_str); + + quote::quote! {}.into() +} + +/// Builds the match-arm entry for `get_metadata`, matching on the enum variant +/// and then on the requested `TypeId`. +fn build_metadata_entry( + entry_type: &TypePath, + metadata_type: &TypePath, +) -> proc_macro2::TokenStream { + let enum_variant = entry_type.path.segments.last().unwrap().ident.clone(); + quote::quote! { + Self::#enum_variant => { + let __metadata_type_id = ::std::any::TypeId::of::<#metadata_type>(); + match type_id { + _ if type_id == __metadata_type_id => Some(::std::boxed::Box::new( + <#entry_type as ::mingling::Metadata<#metadata_type>>::init_metadata(), + ) as ::std::boxed::Box<dyn ::std::any::Any>), + _ => None, + } + } + } +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index c955e36..ce3455e 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -165,7 +165,7 @@ use attr::completion; use attr::dispatcher_clap; #[cfg(feature = "extras")] use attr::program_setup; -use attr::{chain, help, renderer}; +use attr::{chain, help, metadata, renderer}; use derive::{enum_tag, grouped}; #[cfg(feature = "extras")] use func::entry; @@ -209,6 +209,7 @@ pub(crate) static RENDERERS: Registry = OnceLock::new(); pub(crate) static CHAINS_EXIST: Registry = OnceLock::new(); pub(crate) static RENDERERS_EXIST: Registry = OnceLock::new(); pub(crate) static HELP_REQUESTS: Registry = OnceLock::new(); +pub(crate) static METADATA: Registry = OnceLock::new(); /// Checks if a variant name already exists in a registered set. /// Returns a `compile_error` token stream if a duplicate is found. @@ -1297,6 +1298,26 @@ pub fn register_help(input: TokenStream) -> TokenStream { func::register_help::register_help(input) } +/// Registers metadata mapping between an enum variant and a metadata type. +/// +/// This macro is used internally by the `#[metadata]` attribute and is also +/// available for manual registration if needed. +/// +/// # Syntax +/// +/// ```rust,ignore +/// register_metadata!(EntryVariant, MetadataType); +/// ``` +/// +/// This adds an entry to the global `METADATA` registry, mapping the enum +/// variant for `EntryVariant` to the metadata provider trait +/// `::mingling::Metadata<MetadataType>`. The entry is consumed by +/// `gen_program!` to generate the `get_metadata` method of `ProgramCollect`. +#[proc_macro] +pub fn register_metadata(input: TokenStream) -> TokenStream { + func::register_metadata::register_metadata_impl(input) +} + /// Registers a dispatcher at compile time for the `dispatch_tree` feature. /// /// This macro is called internally by `dispatcher!` when the `dispatch_tree` @@ -1404,6 +1425,45 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { help::help_attr(item) } +/// Declares compile-time metadata for an entry variant. +/// +/// The `#[metadata]` attribute attaches an arbitrary, compile-time-typed value +/// to an entry. The annotated function becomes the provider for the metadata: +/// its return type is the metadata type, and the attribute argument names the +/// entry enum variant the metadata belongs to. +/// +/// The macro works by: +/// 1. Generating `impl ::mingling::Metadata<ReturnType> for EntryVariant` whose +/// `init_metadata()` calls the annotated function. +/// 2. Registering the entry via `register_metadata!` in the global `METADATA` +/// registry so that `gen_program!` emits the `get_metadata` method. +/// 3. Keeping the original function unchanged for direct calls. +/// +/// # Syntax +/// +/// ```rust,ignore +/// #[metadata(EntryGreet)] +/// fn greet_desc() -> Description { +/// Description { desc: "ok".into() } +/// } +/// ``` +/// +/// The metadata is later retrieved with `ProgramCollect::get_metadata`: +/// +/// ```rust,ignore +/// let desc = ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet); +/// ``` +/// +/// # Requirements +/// +/// - The attribute argument must be the enum variant to attach metadata to. +/// - The function must take no parameters and return a concrete type. +/// - The function cannot be async. +#[proc_macro_attribute] +pub fn metadata(attr: TokenStream, item: TokenStream) -> TokenStream { + metadata::metadata_attr(attr, item) +} + /// Marker attribute for the Mingling lint system. /// /// The content of this attribute is ignored by rustc and reserved for |
