aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-17 02:29:30 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-17 02:39:56 +0800
commit6980fbf2f9fb4c599d8dc6ff549a8b3288eb24e9 (patch)
treebc7049698955d7a40706ea691fd5bab526a6b5e5 /mingling_macros/src
parent4c191b2b46a67a0dda6e9a867b40f45156af4f9c (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.rs9
-rw-r--r--mingling_macros/src/attr/dispatcher_clap.rs24
-rw-r--r--mingling_macros/src/func/dispatcher.rs22
-rw-r--r--mingling_macros/src/func/program_comp_gen.rs4
-rw-r--r--mingling_macros/src/func/program_final_gen.rs87
-rw-r--r--mingling_macros/src/func/register_dispatcher.rs20
-rw-r--r--mingling_macros/src/lib.rs162
-rw-r--r--mingling_macros/src/systems.rs2
-rw-r--r--mingling_macros/src/systems/dispatch_list_gen.rs57
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs32
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();