diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-17 02:29:30 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-17 02:39:56 +0800 |
| commit | 6980fbf2f9fb4c599d8dc6ff549a8b3288eb24e9 (patch) | |
| tree | bc7049698955d7a40706ea691fd5bab526a6b5e5 /mingling_macros/src | |
| parent | 4c191b2b46a67a0dda6e9a867b40f45156af4f9c (diff) | |
refactor!: remove dynamic dispatcher registration API
Dispatchers are now always registered at compile time, removing the
`with_dispatcher` / `with_dispatchers` methods and the
`PathfinderConfig` API. The `dispatch_tree` feature now only controls
the matching strategy (trie vs linear list).
Diffstat (limited to 'mingling_macros/src')
| -rw-r--r-- | mingling_macros/src/attr/command.rs | 9 | ||||
| -rw-r--r-- | mingling_macros/src/attr/dispatcher_clap.rs | 24 | ||||
| -rw-r--r-- | mingling_macros/src/func/dispatcher.rs | 22 | ||||
| -rw-r--r-- | mingling_macros/src/func/program_comp_gen.rs | 4 | ||||
| -rw-r--r-- | mingling_macros/src/func/program_final_gen.rs | 87 | ||||
| -rw-r--r-- | mingling_macros/src/func/register_dispatcher.rs | 20 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 162 | ||||
| -rw-r--r-- | mingling_macros/src/systems.rs | 2 | ||||
| -rw-r--r-- | mingling_macros/src/systems/dispatch_list_gen.rs | 57 | ||||
| -rw-r--r-- | mingling_macros/src/systems/dispatch_tree_gen.rs | 32 |
10 files changed, 160 insertions, 259 deletions
diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs index c2ddde3..4542bd7 100644 --- a/mingling_macros/src/attr/command.rs +++ b/mingling_macros/src/attr/command.rs @@ -321,19 +321,16 @@ pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream fn_name.span(), ); - // dispatcher internal static (only exists with dispatch_tree feature) - #[cfg(feature = "dispatch_tree")] + // dispatcher internal static (always exists now that dispatchers are + // collected at compile time regardless of the `dispatch_tree` feature) let snaked_node = just_fmt::snake_case!(names.node_lit.value()); - #[cfg(feature = "dispatch_tree")] let dispatcher_internal = { let ident = Ident::new( - &format!("__internal_dispatcher_{}", snaked_node), + &format!("__internal_dispatcher_{snaked_node}"), fn_name.span(), ); quote! { #vis use super::#ident; } }; - #[cfg(not(feature = "dispatch_tree"))] - let dispatcher_internal = quote! {}; let cmd_name = &names.cmd_name; let entry_type = &names.entry_type; diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 9aa7779..2f45b14 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -174,8 +174,8 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke None }; - let dispatch_tree_entry = - get_dispatch_tree_entry(&command_name_str, dispatcher_struct, struct_name); + let compile_time_registration = + get_compile_time_registration(&command_name_str, dispatcher_struct, struct_name); let expanded = quote! { // Keep the original struct definition @@ -187,8 +187,8 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke // Generate the help block if enabled #help_gen - // Dispatch tree registration (if feature enabled) - #dispatch_tree_entry + // Compile-time registration + #compile_time_registration // Generate the dispatcher struct #[doc(hidden)] @@ -223,8 +223,11 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke expanded.into() } -#[cfg(feature = "dispatch_tree")] -fn get_dispatch_tree_entry( +/// Registers the dispatcher at compile time (collects its node into the +/// global `COMPILE_TIME_DISPATCHERS` registry and emits the +/// `__internal_dispatcher_*` static), regardless of the `dispatch_tree` +/// feature. +fn get_compile_time_registration( command_name_str: &str, dispatcher_struct: &Ident, entry_name: &Ident, @@ -234,12 +237,3 @@ fn get_dispatch_tree_entry( ::mingling::macros::register_dispatcher!(#node_name_lit, #dispatcher_struct, #entry_name); } } - -#[cfg(not(feature = "dispatch_tree"))] -fn get_dispatch_tree_entry( - _command_name_str: &str, - _dispatcher_struct: &Ident, - _entry_name: &Ident, -) -> proc_macro2::TokenStream { - quote! {} -} diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index b5e711e..6183993 100644 --- a/mingling_macros/src/func/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -111,7 +111,8 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { let comp_entry = get_comp_entry(&pack); - let dispatch_tree_entry = get_dispatch_tree_entry(&command_name_str, &command_struct, &pack); + let compile_time_registration = + get_compile_time_registration(&command_name_str, &command_struct, &pack); let program_type = crate::default_program_path(); @@ -129,7 +130,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { } #comp_entry - #dispatch_tree_entry + #compile_time_registration impl ::mingling::Dispatcher<#program_type> for #command_struct { fn node(&self) -> ::mingling::Node { @@ -165,8 +166,12 @@ fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 { quote! {} } -#[cfg(feature = "dispatch_tree")] -fn get_dispatch_tree_entry( +/// Registers the dispatcher at compile time (collects its node into the +/// global `COMPILE_TIME_DISPATCHERS` registry and emits the +/// `__internal_dispatcher_*` static), regardless of the `dispatch_tree` +/// feature. The feature only selects which matching strategy +/// (trie vs. linear list) is generated later by `gen_program!`. +fn get_compile_time_registration( command_name_str: &str, command_struct: &Ident, entry_name: &Ident, @@ -176,12 +181,3 @@ fn get_dispatch_tree_entry( ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name); } } - -#[cfg(not(feature = "dispatch_tree"))] -fn get_dispatch_tree_entry( - _command_name_str: &str, - _command_struct: &Ident, - _entry_name: &Ident, -) -> TokenStream2 { - quote! {} -} diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs index d9001ad..7f77d46 100644 --- a/mingling_macros/src/func/program_comp_gen.rs +++ b/mingling_macros/src/func/program_comp_gen.rs @@ -40,14 +40,10 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { } }; - #[cfg(feature = "dispatch_tree")] let internal_dispatcher_comp = quote! { use __internal_completion_mod::__internal_dispatcher_comp; }; - #[cfg(not(feature = "dispatch_tree"))] - let internal_dispatcher_comp = quote! {}; - let comp_dispatcher = quote! { #[doc(hidden)] mod __internal_completion_mod { diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs index 25bca5e..d549a2b 100644 --- a/mingling_macros/src/func/program_final_gen.rs +++ b/mingling_macros/src/func/program_final_gen.rs @@ -4,7 +4,6 @@ use quote::quote; use crate::CHAINS; use crate::CHAINS_EXIST; -#[cfg(feature = "dispatch_tree")] use crate::COMPILE_TIME_DISPATCHERS; #[cfg(feature = "comp")] use crate::COMPLETIONS; @@ -16,6 +15,8 @@ use crate::RENDERERS_EXIST; #[cfg(feature = "structural_renderer")] use crate::STRUCTURAL_RENDERERS; use crate::get_global_set; +#[cfg(not(feature = "dispatch_tree"))] +use crate::systems::dispatch_list_gen; #[cfg(feature = "dispatch_tree")] use crate::systems::dispatch_tree_gen; @@ -24,6 +25,34 @@ const ASYNC_ENABLED: bool = true; #[cfg(not(feature = "async"))] const ASYNC_ENABLED: bool = false; +/// Generate the `get_nodes()` function body for a `ProgramCollect` impl. +/// +/// Shared by both dispatch strategies (trie and linear list); it only depends +/// on the compile-time-collected `__internal_dispatcher_*` statics. +fn gen_get_nodes(entries: &[(String, String, String)]) -> proc_macro2::TokenStream { + let mut node_entries = Vec::new(); + + for (node_name, _disp_type, _entry_name) in entries { + let static_name_str = format!("__internal_dispatcher_{}", just_fmt::snake_case!(node_name)); + let static_ident = + proc_macro2::Ident::new(&static_name_str, proc_macro2::Span::call_site()); + let node_display_name = node_name.replace('.', " "); + let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site()); + + node_entries.push(quote! { + (#node_display_lit.to_string(), &#static_ident) + }); + } + + quote! { + fn get_nodes() -> Vec<(String, &'static (dyn ::mingling::Dispatcher<Self::Enum> + Send + Sync))> { + vec![ + #(#node_entries),* + ] + } + } +} + /// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { let s = entry.to_string(); @@ -117,7 +146,6 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { #[cfg(not(feature = "structural_renderer"))] let structural_render = quote! {}; - #[cfg(feature = "dispatch_tree")] let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) .lock() .unwrap() @@ -126,35 +154,45 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { .cloned() .collect(); - #[cfg(feature = "dispatch_tree")] - let dispatch_tree_nodes = { - let entries: Vec<(String, String, String)> = compile_time_dispatchers - .iter() - .filter_map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - if parts.len() == 3 { - Some(( - parts[0].to_string(), - parts[1].to_string(), - parts[2].to_string(), - )) - } else { - None - } - }) - .collect(); + let entries: Vec<(String, String, String)> = compile_time_dispatchers + .iter() + .filter_map(|entry| { + let parts: Vec<&str> = entry.split(':').collect(); + if parts.len() == 3 { + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + )) + } else { + None + } + }) + .collect(); - let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries); - let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries); + // The `dispatch_tree` feature only selects the internal matching strategy: + // a char-level trie when enabled, a linear longest-prefix list otherwise. + #[cfg(feature = "dispatch_tree")] + let dispatch_gen = { + let get_nodes_fn = gen_get_nodes(&entries); + let dispatch_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries); quote! { #get_nodes_fn - #dispatch_trie_fn + #dispatch_fn } }; #[cfg(not(feature = "dispatch_tree"))] - let dispatch_tree_nodes = quote! {}; + let dispatch_gen = { + let get_nodes_fn = gen_get_nodes(&entries); + let dispatch_fn = dispatch_list_gen::gen_dispatch_args(&entries); + + quote! { + #get_nodes_fn + #dispatch_fn + } + }; #[cfg(feature = "comp")] let completion_tokens: Vec<proc_macro2::TokenStream> = completions @@ -367,7 +405,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { _ => false } } - #dispatch_tree_nodes + #dispatch_gen #structural_render #comp } @@ -395,7 +433,6 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { get_global_set(&METADATA).lock().unwrap().clear(); #[cfg(feature = "comp")] get_global_set(&COMPLETIONS).lock().unwrap().clear(); - #[cfg(feature = "dispatch_tree")] get_global_set(&COMPILE_TIME_DISPATCHERS) .lock() .unwrap() diff --git a/mingling_macros/src/func/register_dispatcher.rs b/mingling_macros/src/func/register_dispatcher.rs index eb4a6e4..dc9a31a 100644 --- a/mingling_macros/src/func/register_dispatcher.rs +++ b/mingling_macros/src/func/register_dispatcher.rs @@ -1,26 +1,19 @@ // Doc Not Optimize -#[cfg(feature = "dispatch_tree")] use just_fmt::snake_case; use proc_macro::TokenStream; use quote::quote; -#[cfg(feature = "dispatch_tree")] use syn::parse::{Parse, ParseStream}; -#[cfg(feature = "dispatch_tree")] use syn::{Ident, LitStr, Result as SynResult, Token}; -#[cfg(feature = "dispatch_tree")] use crate::COMPILE_TIME_DISPATCHERS; -#[cfg(feature = "dispatch_tree")] use crate::get_global_set; -#[cfg(feature = "dispatch_tree")] struct RegisterDispatcherInput { node_name: LitStr, dispatcher_type: Ident, entry_name: Ident, } -#[cfg(feature = "dispatch_tree")] impl Parse for RegisterDispatcherInput { fn parse(input: ParseStream) -> SynResult<Self> { let node_name: LitStr = input.parse()?; @@ -28,7 +21,7 @@ impl Parse for RegisterDispatcherInput { let dispatcher_type: Ident = input.parse()?; input.parse::<Token![,]>()?; let entry_name: Ident = input.parse()?; - Ok(RegisterDispatcherInput { + Ok(Self { node_name, dispatcher_type, entry_name, @@ -36,7 +29,6 @@ impl Parse for RegisterDispatcherInput { } } -#[cfg(feature = "dispatch_tree")] pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { let RegisterDispatcherInput { node_name, @@ -56,10 +48,7 @@ pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { get_global_set(&COMPILE_TIME_DISPATCHERS) .lock() .unwrap() - .insert(format!( - "{}:{}:{}", - node_name_str, dispatcher_type, entry_name - )); + .insert(format!("{node_name_str}:{dispatcher_type}:{entry_name}")); let expanded = quote! { #[doc(hidden)] @@ -69,8 +58,3 @@ pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { expanded.into() } - -#[cfg(not(feature = "dispatch_tree"))] -pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { - quote! {}.into() -} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index bf3e650..ad84548 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -1,146 +1,9 @@ -// Doc Not Optimize //! Proc-macro engine of the Mingling CLI framework. //! //! This crate is the **macro layer** of Mingling. Each `#[attribute]` or `!`-callable //! macro collects metadata into **compile-time global registries** (`OnceLock<Mutex<BTreeSet>>`). //! At the end, `gen_program!` reads all registries and generates the final program struct //! with all dispatchers, chains, renderers, and completions wired together. -//! -//! # How Macros Work Together -//! -//! The Mingling macro pipeline has three phases: -//! -//! ```text -//! ┌──────────────────────────────────────────────────────────────────┐ -//! │ Phase 1: Declaration │ -//! │ │ -//! │ dispatcher! pack! node! #[derive(Grouped)] │ -//! │ │ │ │ │ │ -//! │ V V V V │ -//! │ Declares Wraps a Builds Makes a type │ -//! │ a command type in a command recognizable │ -//! │ entry a new path Node by the │ -//! │ type framework │ -//! ├──────────────────────────────────────────────────────────────────┤ -//! │ Phase 2: Registration (at compile time, in statics) │ -//! │ │ -//! │ #[chain] #[renderer] #[help] #[completion] │ -//! │ │ │ │ │ │ -//! │ V V V V │ -//! │ Registers Registers Registers Registers │ -//! │ type → chain type → renderer type → help completion logic │ -//! ├──────────────────────────────────────────────────────────────────┤ -//! │ Phase 3: Code Generation │ -//! │ │ -//! │ gen_program!() │ -//! │ │ │ -//! │ V │ -//! │ Reads all registries → generates ThisProgram with: │ -//! │ • ProgramCollect impl (dispatch/render/chain dispatch tree) │ -//! │ • Fallback types (EntryFallback, etc.) │ -//! │ • Completion logic (if `comp` feature enabled) │ -//! └──────────────────────────────────────────────────────────────────┘ -//! ``` -//! -//! # Macro Categories -//! -//! ## Phase 1: Command & Type Declaration -//! -//! | Macro | What it does | -//! |-------|-------------| -//! | `dispatcher!` | Declares a command entry point and its argument type | -//! | `dispatcher_clap!` | Like `dispatcher!` but powered by `clap::Parser` | -//! | `node!` | Builds a [`Node`](https://docs.rs/mingling/latest/mingling/struct.Node.html) from a dot-separated path string | -//! | `pack!` | Creates a newtype wrapper around an inner type for use in Chain/Renderer | -//! | `pack_structural!` | Like `pack!` but also derives `StructuralData` for structured output | -//! | `pack_err!` | Creates an error struct with automatic `name` field | -//! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output | -//! | `entry!` | Creates a packed entry from string literals | -//! | [`#[derive(Grouped)]`](derive@Grouped) | Makes a type recognizable by the framework's type registry | -//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) | -//! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) | -//! -//! ## Phase 2: Processing & Rendering Registration -//! -//! | Macro | What it does | -//! |-------|-------------| -//! | [`#[chain]`](attr.chain.html) | Transforms a function into a chain processing step | -//! | [`#[renderer]`](attr.renderer.html) | Transforms a function into a renderer for a type | -//! | [`#[help]`](attr.help.html) | Defines help output for a command entry type | -//! | `route!` | Routes execution depending on a condition | -//! | `empty_result!` | Returns an empty result for early termination | -//! | [`#[completion]`](attr.completion.html) | Registers a shell completion handler | -//! -//! ## Phase 3: Program Generation -//! -//! | Macro | What it does | -//! |-------|-------------| -//! | `gen_program!` | **Final step**: reads all registries and generates the full program | -//! | `suggest!` | Generates suggestion logic for a dispatcher | -//! | `suggest_enum!` | Generates suggestion logic for an enum dispatcher | -//! -//! ## Internal (used by the macros above) -//! -//! | Macro | What it does | -//! |-------|-------------| -//! | `register_type!` | Registers a type in the packed-type registry | -//! | `register_chain!` | Registers a chain mapping in the chain registry | -//! | `register_renderer!` | Registers a renderer mapping in the renderer registry | -//! | `register_dispatcher!` | Registers a dispatcher for the `dispatch_tree` feature | -//! | `register_help!` | Registers a help request handler | -//! | `program_fallback_gen!` | Generates fallback error types | -//! | `program_final_gen!` | Generates the `ProgramCollect` impl and `ThisProgram` struct | -//! | `program_comp_gen!` | Generates completion logic | -//! | [`#[program_setup]`](attr.program_setup.html) | Declares a custom program setup step | -//! -//! # Feature Gates -//! -//! Some macros are only available when certain Cargo features are enabled: -//! -//! | Feature | Macros enabled | -//! |---------|---------------| -//! | `clap` | `dispatcher_clap!` | -//! | `comp` | [`#[completion]`](attr.completion.html), `suggest!`, `suggest_enum!` | -//! | `extras` | `entry!`, `empty_result!`, `route!`, [`#[program_setup]`](attr.program_setup.html), `group!` | -//! | `dispatch_tree` | `register_dispatcher!` (enables trie-based command dispatch) | -//! | `structural_renderer` | `#[derive(StructuralData)]`, `pack_structural!`, `pack_err_structural!`, `group_structural!` | -//! | `structural_renderer` + `extras` | `group_structural!`, `pack_err_structural!` | -//! | `async` | Enables async `#[chain]` functions | -//! | `repl` | Enables REPL execution loop | -//! -//! # The Compile-Time Registry System -//! -//! Macros in this crate do **not** generate all code immediately. Instead, they -//! store entries into `OnceLock<Mutex<BTreeSet<String>>>` statics. These string -//! entries contain the **token-stream representation** of match arms, type mappings, -//! and struct definitions. -//! -//! When `gen_program!` is called, it reads all registries, concatenates their -//! entries, and emits the complete program: -//! -//! ```rust,ignore -//! // Example of what gen_program! generates (simplified): -//! impl ProgramCollect for ThisProgram { -//! fn build_entry_fallback(args: Vec<String>) -> AnyOutput { -//! AnyOutput::new(EntryFallback::new(args)) -//! } -//! fn has_chain(any: &AnyOutput) -> bool { -//! match any.member_id() { -//! MyType => true, // ← collected from #[chain] macros -//! _ => false, -//! } -//! } -//! fn has_renderer(any: &AnyOutput) -> bool { -//! match any.member_id() { -//! MyType => true, // ← collected from #[renderer] macros -//! // When `structural_renderer` is enabled, ALL registered types -//! // return true — non-structural types fall through to render -//! // a `ResultEmpty` value (via structural_render fallback). -//! _ => false, -//! } -//! } -//! } -//! ``` #![deny(missing_docs)] #![deny(clippy::pedantic)] @@ -204,7 +67,6 @@ pub(crate) static STRUCTURED_TYPES: Registry = OnceLock::new(); #[cfg(feature = "comp")] pub(crate) static COMPLETIONS: Registry = OnceLock::new(); -#[cfg(feature = "dispatch_tree")] pub(crate) static COMPILE_TIME_DISPATCHERS: Registry = OnceLock::new(); pub(crate) static PACKED_TYPES: Registry = OnceLock::new(); @@ -740,8 +602,9 @@ pub fn empty_result(input: TokenStream) -> TokenStream { /// - `node()` returns the [`Node`] hierarchy for the command path. /// - `begin(args)` wraps `args` into the entry type and routes to chain. /// - `clone_dispatcher()` returns a boxed clone. -/// 3. **Registration** — If the `dispatch_tree` feature is enabled, also calls -/// `register_dispatcher!` for compile-time trie construction. +/// 3. **Registration** — Calls `register_dispatcher!` to collect the command +/// at compile time (the `dispatch_tree` feature only selects the matching +/// strategy generated later by `gen_program!`). /// /// With the `comp` feature, the entry type also implements `CompletionEntry` /// for providing shell completion suggestions. @@ -1322,13 +1185,14 @@ 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. +/// Registers a dispatcher at compile time. /// -/// This macro is called internally by `dispatcher!` when the `dispatch_tree` -/// feature is enabled. Each call stores the node name into the global -/// `COMPILE_TIME_DISPATCHERS` registry and generates a static variable for the -/// dispatcher instance. This data is later consumed by `gen_program!` to -/// generate a character-level **Trie** for efficient command dispatch. +/// This macro is called internally by `dispatcher!` and `dispatcher_clap!`. +/// Each call stores the node name into the global `COMPILE_TIME_DISPATCHERS` +/// registry and generates a static variable for the dispatcher instance. This +/// data is later consumed by `gen_program!` to generate command matching: a +/// character-level **trie** when the `dispatch_tree` feature is enabled, or a +/// linear longest-prefix list otherwise. /// /// The trie dispatch works by grouping commands by their character prefix, /// enabling O(n) lookup (where n is input length) instead of linear iteration @@ -1345,7 +1209,7 @@ pub fn register_metadata(input: TokenStream) -> TokenStream { /// # See also /// /// - `dispatcher!` — The primary way to declare dispatchers (calls this internally). -/// - `dispatch_tree_gen` module — The trie generation logic. +/// - `dispatch_tree_gen` / `dispatch_list_gen` modules — The matching-strategy generators. #[proc_macro] pub fn register_dispatcher(input: TokenStream) -> TokenStream { func::register_dispatcher::register_dispatcher(input) @@ -1935,8 +1799,8 @@ pub fn gen_program(input: TokenStream) -> TokenStream { /// 3. An internal renderer `__render_completion` that renders the suggestions via /// `CompletionHelper::render_suggest`. /// -/// When the `dispatch_tree` feature is enabled, it also imports the internal dispatcher -/// from the generated module into the parent scope for trie-based dispatch. +/// It also imports the internal dispatcher from the generated module into the +/// parent scope for compile-time collection. /// /// This macro is called automatically by `gen_program!` and should not be called /// directly by user code. diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs index 44478a0..ef3624e 100644 --- a/mingling_macros/src/systems.rs +++ b/mingling_macros/src/systems.rs @@ -1,4 +1,6 @@ // Doc Not Optimize +#[cfg(not(feature = "dispatch_tree"))] +pub(crate) mod dispatch_list_gen; #[cfg(feature = "dispatch_tree")] pub(crate) mod dispatch_tree_gen; pub(crate) mod res_injection; diff --git a/mingling_macros/src/systems/dispatch_list_gen.rs b/mingling_macros/src/systems/dispatch_list_gen.rs new file mode 100644 index 0000000..52b0e86 --- /dev/null +++ b/mingling_macros/src/systems/dispatch_list_gen.rs @@ -0,0 +1,57 @@ +// Doc Not Optimize +use std::cmp::Reverse; + +use proc_macro2::TokenStream; +use quote::quote; + +/// Generate the `dispatch_args()` function body for a `ProgramCollect` impl +/// using linear matching over the compile-time-collected dispatchers. +/// +/// Nodes are sorted by display-name length (longest first) so the first +/// matching node is the most specific one, mirroring the "longest registered +/// prefix wins" rule of the old dynamic dispatcher. +pub(crate) fn gen_dispatch_args(entries: &[(String, String, String)]) -> TokenStream { + let mut nodes: Vec<(String, String)> = entries + .iter() + .map(|(name, disp, _)| (name.replace('.', " "), disp.clone())) + .collect(); + nodes.sort_by_key(|(name, _)| Reverse(name.len())); + + let arms: Vec<TokenStream> = nodes + .iter() + .map(|(name, disp_type)| { + let name_space = format!("{name} "); + let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site()); + let disp_ident = proc_macro2::Ident::new(disp_type, proc_macro2::Span::call_site()); + let prefix_word_count = name.split_whitespace().count(); + quote! { + if raw_str.starts_with(#name_lit) { + let prefix_len = #prefix_word_count; + let trimmed_args: Vec<String> = raw.iter().skip(prefix_len).cloned().collect(); + let __cp = <#disp_ident as ::mingling::Dispatcher<Self::Enum>>::begin( + &#disp_ident::default(), + trimmed_args, + ); + return match __cp { + ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0), + ::mingling::ChainProcess::Err(chain_process_error) => { + Err(chain_process_error.into()) + } + }; + } + } + }) + .collect(); + + quote! { + fn dispatch_args( + raw: &[String], + ) -> Result<::mingling::AnyOutput<Self::Enum>, ::mingling::error::ProgramInternalExecuteError> + { + let raw_string = format!("{} ", raw.join(" ")); + let raw_str = raw_string.as_str(); + #(#arms)* + Ok(Self::build_entry_fallback(raw.to_vec())) + } + } +} diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index d157feb..2b264f7 100644 --- a/mingling_macros/src/systems/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -1,36 +1,10 @@ // Doc Not Optimize use std::collections::BTreeMap; -use just_fmt::snake_case; use proc_macro2::TokenStream; use quote::quote; -/// Generate the `get_nodes()` function body for a ProgramCollect impl. -pub(crate) fn gen_get_nodes(entries: &[(String, String, String)]) -> TokenStream { - let mut node_entries = Vec::new(); - - for (node_name, _disp_type, _entry_name) in entries { - let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name)); - let static_ident = - proc_macro2::Ident::new(&static_name_str, proc_macro2::Span::call_site()); - let node_display_name = node_name.replace('.', " "); - let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site()); - - node_entries.push(quote! { - (#node_display_lit.to_string(), &#static_ident) - }); - } - - quote! { - fn get_nodes() -> Vec<(String, &'static (dyn ::mingling::Dispatcher<Self::Enum> + Send + Sync))> { - vec![ - #(#node_entries),* - ] - } - } -} - -/// Generate the `dispatch_args()` function body for a ProgramCollect impl. +/// Generate the `dispatch_args()` function body for a `ProgramCollect` impl. /// /// Builds a hardcoded match tree: at each depth, group nodes by character. /// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match. @@ -63,7 +37,7 @@ pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> To /// Recursively build the trie match body. /// -/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far. +/// `nodes`: slice of (`display_name`, `disp_type`) for commands that share the same prefix so far. /// `depth`: The character index currently being matched. /// `no_match`: fallback code to run when no node in this subtree matches the input. /// @@ -95,7 +69,7 @@ fn build_dispatch_body( } let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream { - let name_space = format!("{} ", name); + let name_space = format!("{name} "); let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site()); let disp_ident = proc_macro2::Ident::new(disp_type, proc_macro2::Span::call_site()); let prefix_word_count = name.split_whitespace().count(); |
