aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/attr.rs1
-rw-r--r--mingling_macros/src/attr/metadata.rs87
-rw-r--r--mingling_macros/src/func.rs1
-rw-r--r--mingling_macros/src/func/program_final_gen.rs35
-rw-r--r--mingling_macros/src/func/register_metadata.rs67
-rw-r--r--mingling_macros/src/lib.rs62
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs78
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,
+ &quote! {
+ 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
+ }
}
}
}