aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-04 13:26:48 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-04 13:26:48 +0800
commit4588e17f7ddacd8391a5c881a209735e08d48797 (patch)
tree2b513f4d4f8a18ebd3a34cff7c0fb8abf5787321 /mingling_macros
parentf7862affd74ac3b129f9e9791a4cd4e1c7e3e6d1 (diff)
feat: add entry metadata system with compile-time typed values
Add `Metadata<B>` trait and `#[metadata(Entry)]` attribute macro to attach arbitrary, compile-time-typed metadata to entries. Implement `ProgramCollect::get_metadata<T>()` for runtime retrieval, backed by a global registry populated by `register_metadata!`. Extend `pathf` with `MetadataPattern` to resolve metadata types across modules at build time. Add two examples demonstrating metadata usage with and without pathf integration.
Diffstat (limited to 'mingling_macros')
-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
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