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 | ||||
| -rw-r--r-- | mingling_macros/src/systems/dispatch_tree_gen.rs | 78 |
7 files changed, 297 insertions, 34 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 diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index 8e3660c..f390b33 100644 --- a/mingling_macros/src/systems/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -39,7 +39,13 @@ pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> To .map(|(name, disp, _)| (name.replace('.', " "), disp.clone())) .collect(); - let dispatch_body = build_dispatch_body(&nodes, 0); + let dispatch_body = build_dispatch_body( + &nodes, + 0, + "e! { + return Ok(Self::build_entry_fallback(raw.to_vec())); + }, + ); quote! { fn dispatch_args_trie( @@ -58,11 +64,19 @@ pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> To /// /// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far. /// `depth`: The character index currently being matched. -fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream { +/// `no_match`: fallback code to run when no node in this subtree matches the input. +/// +/// Matching follows the same "longest registered prefix" rule used by the +/// dynamic dispatcher: a child (longer) path is preferred over an exact +/// endpoint at the same depth. Only when every descendant fails to match is +/// the exact endpoint here dispatched. +fn build_dispatch_body( + nodes: &[(String, String)], + depth: usize, + no_match: &TokenStream, +) -> TokenStream { if nodes.is_empty() { - return quote! { - return Ok(Self::build_entry_fallback(raw.to_vec())); - }; + return no_match.clone(); } let mut groups: BTreeMap<char, Vec<(String, String)>> = BTreeMap::new(); @@ -102,6 +116,20 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream } }; + // Fallback code for when neither a child path nor the exact endpoint(s) + // here match: run the exact endpoint checks for this node first (they must + // win over nothing at all), then pass control back up to the caller. + let exact_checks: Vec<TokenStream> = exact_nodes + .iter() + .map(|(name, disp_type)| make_starts_with_arm(name, disp_type)) + .collect(); + + let level_no_match = { + let mut body = exact_checks.clone(); + body.push(no_match.clone()); + quote! { #(#body)* } + }; + let mut arms = Vec::new(); for (&ch, sub_nodes) in &groups { @@ -110,14 +138,16 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream if sub_nodes.len() == 1 { let (name, disp_type) = &sub_nodes[0]; let arm = make_starts_with_arm(name, disp_type); + // Try the child first; if it does not match, fall through to the + // exact endpoint(s) here so the longer path wins when present. arms.push(quote! { Some(#ch_char) => { #arm - return Ok(Self::build_entry_fallback(raw.to_vec())); + #level_no_match } }); } else { - let sub_body = build_dispatch_body(sub_nodes, depth + 1); + let sub_body = build_dispatch_body(sub_nodes, depth + 1, &level_no_match); arms.push(quote! { Some(#ch_char) => { #sub_body @@ -126,36 +156,18 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream } } - let exact_checks: Vec<TokenStream> = exact_nodes - .iter() - .map(|(name, disp_type)| make_starts_with_arm(name, disp_type)) - .collect(); - - if !exact_checks.is_empty() && !groups.is_empty() { - let match_body = quote! { - match raw_chars.nth(0) { - #(#arms)* - _ => return Ok(Self::build_entry_fallback(raw.to_vec())), - } - }; - quote! { - #(#exact_checks)* - #match_body - } - } else if !exact_checks.is_empty() { - quote! { - #(#exact_checks)* - return Ok(Self::build_entry_fallback(raw.to_vec())); - } - } else if arms.is_empty() { - quote! { - return Ok(Self::build_entry_fallback(raw.to_vec())); - } + if groups.is_empty() { + // No children exist for this node; only the exact endpoint(s) apply. + let mut body = exact_checks; + body.push(no_match.clone()); + quote! { #(#body)* } } else { quote! { match raw_chars.nth(0) { #(#arms)* - _ => return Ok(Self::build_entry_fallback(raw.to_vec())), + _ => { + #level_no_match + } } } } |
