aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/systems
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-20 03:38:10 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-20 03:38:10 +0800
commit5678e0ba37ce2c5c8161f0d48505e7993967e30c (patch)
treee6035d2e8bf4139e634052fef8c0c48d4c97434a /mingling_macros/src/systems
parentec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 (diff)
feat: add dispatch strategy features with auto-selection and benchmark
support Add `dispatch_linear`, `dispatch_tree`, and `dispatch_phf` as mutually exclusive features for selecting the command dispatch strategy. When no strategy feature is enabled, an auto-selection heuristic picks the optimal strategy based on command table characteristics (size, name length, nesting depth). Add the `bench_support` feature to compile all three generators for the benchmark harness, and introduce `bench_cell!` to generate benchmark cells comparing strategies. The PHF generator uses a CHD minimal perfect hash for O(1) lookup independent of table size, while the trie generator was refactored to use a shared fallback method, keeping code size linear in table size.
Diffstat (limited to 'mingling_macros/src/systems')
-rw-r--r--mingling_macros/src/systems/dispatch_auto.rs110
-rw-r--r--mingling_macros/src/systems/dispatch_phf_gen.rs260
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs288
3 files changed, 582 insertions, 76 deletions
diff --git a/mingling_macros/src/systems/dispatch_auto.rs b/mingling_macros/src/systems/dispatch_auto.rs
new file mode 100644
index 0000000..54784e2
--- /dev/null
+++ b/mingling_macros/src/systems/dispatch_auto.rs
@@ -0,0 +1,110 @@
+// Doc Not Optimize
+//! Auto dispatch-strategy selection (no dispatch feature enabled).
+//!
+//! Picks the matching strategy at macro-expansion time from the normalized
+//! command table. The rules are calibrated against the `dev/bench/dispatch`
+//! matrix (len 4/8/16/32 × count 128/256 × single/multi/nested4/nested10),
+//! where "optimal" is the per-cell minimum of hit×miss geomean:
+//!
+//! - deep nested chains at modest sizes (`max_words ≥ 8`, `n ≤ 128`) →
+//! **linear list**: a few short memcmps beat the trie's per-level `nth(0)`
+//! walk plus the fallback call on non-leaf hits;
+//! - single-word tables with long names → **perfect hash**: one hash beats
+//! the trie's char walk once names grow past ~16 chars;
+//! - small tables (`n ≤ 64`) → linear vs trie by a cost model (linear wins
+//! on short names, loses once `count × length` grows);
+//! - everything else → **char trie** (O(depth) hit cost independent of table
+//! size, best miss path, linear code size after the fallback-chain fix).
+
+/// Linear per-check memcmp cost: `LIN_A + LIN_B × len` ns, plus a miss term
+/// that shrinks as names grow (long names are rejected by the length
+/// precheck on the miss path).
+const LIN_A: f64 = 0.5;
+const LIN_B: f64 = 0.04;
+const LIN_MISS_A: f64 = 0.18;
+const LIN_MISS_B: f64 = 0.005;
+
+/// Trie per-command overhead: `TRIE_FIXED + TRIE_PER_CHAR × max_len` plus an
+/// extra term when exact endpoints exist at multiple depths (nested chains
+/// force a fallback call on non-leaf hits).
+const TRIE_FIXED: f64 = 25.0;
+const TRIE_PER_CHAR: f64 = 1.5;
+const TRIE_NESTED: f64 = 25.0;
+const TRIE_MISS: f64 = 3.0;
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub(crate) enum DispatchStrategy {
+ Linear,
+ Trie,
+ Phf,
+}
+
+/// Pick the dispatch strategy for a normalized entry table.
+///
+/// Casting `usize` counts to `f64` is intentional: these are cost-model
+/// heuristics, not exact numeric computations. Any rounding error is tiny
+/// relative to the measured performance margins between strategies, so
+/// precision loss here is harmless.
+#[allow(clippy::cast_precision_loss)]
+pub(crate) fn select_strategy(entries: &[(String, String, String)]) -> DispatchStrategy {
+ if entries.is_empty() {
+ return DispatchStrategy::Linear;
+ }
+
+ let mut names: Vec<String> = Vec::with_capacity(entries.len());
+ for (name, _, _) in entries {
+ names.push(name.replace('.', " "));
+ }
+
+ let n = names.len() as f64;
+ let avg_len = names.iter().map(|s| s.chars().count()).sum::<usize>() as f64 / n;
+ let max_len = names.iter().map(|s| s.chars().count()).max().unwrap_or(0) as f64;
+ let max_words = names
+ .iter()
+ .map(|s| s.split_whitespace().count())
+ .max()
+ .unwrap_or(0);
+
+ // Nested prefix chains (a command that is a strict prefix of another)
+ // make the trie fall back on non-leaf hits.
+ let mut sorted: Vec<&String> = names.iter().collect();
+ sorted.sort();
+ let nested = sorted.windows(2).any(|w| w[1].starts_with(w[0].as_str()));
+
+ // Nested tables at modest sizes: decide linear vs trie by the cost
+ // model. The linear list wins on short names (its few memcmps beat the
+ // trie's char walk plus fallback calls) and loses once names grow.
+ if nested && n <= 128.0 {
+ let miss_linear = n * LIN_MISS_B.mul_add(-avg_len, LIN_MISS_A).max(0.0);
+ let cost_linear = (n / 2.0).mul_add(LIN_B.mul_add(avg_len, LIN_A), miss_linear);
+ let cost_trie = TRIE_PER_CHAR.mul_add(max_len, TRIE_FIXED) + TRIE_NESTED + TRIE_MISS;
+ return if cost_linear < cost_trie {
+ DispatchStrategy::Linear
+ } else {
+ DispatchStrategy::Trie
+ };
+ }
+
+ // Single-word tables with long names: one hash beats the char walk once
+ // names grow past ~16 chars (measured on single-word len16/32).
+ if max_words == 1 && (avg_len >= 24.0 || (n <= 128.0 && avg_len >= 16.0)) {
+ return DispatchStrategy::Phf;
+ }
+
+ // Small tables: linear vs trie by the cost model. The linear list wins
+ // on short names but collapses as `count × length` grows.
+ if n <= 64.0 {
+ let miss_linear = n * LIN_MISS_B.mul_add(-avg_len, LIN_MISS_A).max(0.0);
+ let cost_linear = (n / 2.0).mul_add(LIN_B.mul_add(avg_len, LIN_A), miss_linear);
+ let cost_trie = TRIE_PER_CHAR.mul_add(max_len, TRIE_FIXED)
+ + if nested { TRIE_NESTED } else { 0.0 }
+ + TRIE_MISS;
+ return if cost_linear < cost_trie {
+ DispatchStrategy::Linear
+ } else {
+ DispatchStrategy::Trie
+ };
+ }
+
+ DispatchStrategy::Trie
+}
diff --git a/mingling_macros/src/systems/dispatch_phf_gen.rs b/mingling_macros/src/systems/dispatch_phf_gen.rs
new file mode 100644
index 0000000..97210e2
--- /dev/null
+++ b/mingling_macros/src/systems/dispatch_phf_gen.rs
@@ -0,0 +1,260 @@
+// Doc Not Optimize
+//! Perfect-hash dispatch generator (`dispatch_phf` feature).
+//!
+//! Generates a `dispatch_args()` body using a **CHD minimal perfect hash**
+//! (Belazzougui, Botelho, Dietzfelbinger) computed at macro-expansion time
+//! over the normalized command names.
+//!
+//! Semantics match `dispatch_list_gen` / `dispatch_tree_gen` exactly:
+//! - the longest registered name that is a word-aligned prefix of the input
+//! wins (scan the input word-by-word from the longest possible prefix down
+//! to the first word);
+//! - every hash hit is verified with an exact byte equality against the
+//! stored key (the hash alone is not a proof of membership);
+//! - duplicate normalized names are dropped, keeping the first registration
+//! ("first wins", same as the other generators).
+//!
+//! Runtime cost per lookup: one byte scan over the first `max_words` words
+//! plus at most `max_words` (typically 1–3) double-hash + verify attempts.
+//! Code size is O(1) in the number of commands.
+
+use std::collections::HashSet;
+
+use proc_macro2::TokenStream;
+use quote::quote;
+
+/// Hash used both at macro time (seed search) and inside the generated code.
+/// FNV-1a over the bytes followed by a splitmix64 finalizer for good
+/// avalanche; fully deterministic and platform independent.
+fn phf_hash(s: &str, seed: u64) -> u64 {
+ let mut h = 0xcbf2_9ce4_8422_2325u64 ^ seed;
+ for &b in s.as_bytes() {
+ h ^= u64::from(b);
+ h = h.wrapping_mul(0x0100_0000_01b3);
+ }
+ h ^= h >> 33;
+ h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
+ h ^= h >> 33;
+ h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
+ h ^= h >> 33;
+ h
+}
+
+/// CHD construction: bucket the keys by `h1`, then per bucket find a
+/// displacement `d` such that `(h0(key) + d) % n` is globally collision free.
+/// Returns `(seed0, seed1, bucket_count, displacements)`.
+///
+/// Note: this function performs `u64` → `usize` casts that may truncate on
+/// 32-bit platforms. This is intentional and acceptable here because the
+/// number of keys is derived from the macro input (command names), which is
+/// far smaller than `u32::MAX` in practice on all supported targets.
+#[allow(clippy::cast_possible_truncation)]
+fn build_chd(keys: &[String]) -> (u64, u64, usize, Vec<u64>) {
+ let n = keys.len();
+ debug_assert!(n > 0);
+ let m = n as u64;
+ let b = n.div_ceil(2).max(1);
+
+ for attempt in 0..10_000u64 {
+ // Fixed magic constants used as seed bases; the `^ attempt * prime`
+ // pattern produces a fresh seed pair on each attempt.
+ #[allow(clippy::unreadable_literal)]
+ let seed0 = 0x9e37_79b9_7f4a_7c15u64 ^ attempt.wrapping_mul(0xbf58_476d_1ce4_e5b9);
+ #[allow(clippy::unreadable_literal)]
+ let seed1 = 0x94d0_49bb_1331_11ebu64 ^ attempt.wrapping_mul(0xd6e8_feb8_6659_fd93);
+
+ let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); b];
+ for (i, key) in keys.iter().enumerate() {
+ let h1 = (phf_hash(key, seed1) % b as u64) as usize;
+ buckets[h1].push(i);
+ }
+
+ // Place the largest buckets first; they have the least freedom.
+ let mut order: Vec<usize> = (0..b).collect();
+ order.sort_by_key(|&j| std::cmp::Reverse(buckets[j].len()));
+
+ let mut disp = vec![0u64; b];
+ let mut occupied = vec![false; m as usize];
+ let mut ok = true;
+
+ 'attempt: for &j in &order {
+ if buckets[j].is_empty() {
+ continue;
+ }
+ for d in 0..m {
+ let mut seen: HashSet<usize> = HashSet::with_capacity(buckets[j].len());
+ for &i in &buckets[j] {
+ let h0 = (phf_hash(&keys[i], seed0) % m) as usize;
+ let slot = (h0 + d as usize) % m as usize;
+ if occupied[slot] || !seen.insert(slot) {
+ break;
+ }
+ }
+ if seen.len() == buckets[j].len() {
+ for &i in &buckets[j] {
+ let h0 = (phf_hash(&keys[i], seed0) % m) as usize;
+ let slot = (h0 + d as usize) % m as usize;
+ occupied[slot] = true;
+ }
+ disp[j] = d;
+ continue 'attempt;
+ }
+ }
+ ok = false;
+ break;
+ }
+
+ if ok {
+ return (seed0, seed1, b, disp);
+ }
+ }
+
+ panic!("dispatch_phf: failed to construct a perfect hash for {n} keys");
+}
+
+// This function is long because it's generating a large body of code we
+// don't want to split; keeping it as one codegen function is far easier to
+// maintain than splitting it into many small pieces.
+#[allow(clippy::too_many_lines)]
+/// Generate the `dispatch_args()` function body for a `ProgramCollect` impl.
+pub(crate) fn gen_dispatch_args_phf(entries: &[(String, String, String)]) -> TokenStream {
+ // Normalize names (dots become spaces), dropping duplicate names while
+ // keeping the first registration ("first wins").
+ let mut seen = HashSet::new();
+ let mut nodes: Vec<(String, String)> = Vec::new();
+ for (name, disp, _) in entries {
+ let name = name.replace('.', " ");
+ if seen.insert(name.clone()) {
+ nodes.push((name, disp.clone()));
+ }
+ }
+
+ let fallback_body = quote! {
+ fn dispatch_args(
+ raw: &[String],
+ ) -> Result<::mingling::AnyOutput<Self::Enum>, ::mingling::error::ProgramInternalExecuteError>
+ {
+ Ok(Self::build_entry_fallback(raw.to_vec()))
+ }
+ };
+ if nodes.is_empty() {
+ return fallback_body;
+ }
+
+ let max_words = nodes
+ .iter()
+ .map(|(name, _)| name.split_whitespace().count())
+ .max()
+ .unwrap();
+ let keys: Vec<String> = nodes.iter().map(|(name, _)| name.clone()).collect();
+ let (seed0, seed1, b, disp) = build_chd(&keys);
+
+ let name_lits: Vec<proc_macro2::Literal> = nodes
+ .iter()
+ .map(|(name, _)| proc_macro2::Literal::string(name))
+ .collect();
+ let disp_lits: Vec<proc_macro2::Literal> = disp
+ .iter()
+ .map(|d| proc_macro2::Literal::u64_unsuffixed(*d))
+ .collect();
+
+ let arms: Vec<TokenStream> = nodes
+ .iter()
+ .enumerate()
+ .map(|(idx, (_, disp_type))| {
+ let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
+ let disp_ident = proc_macro2::Ident::new(disp_type, proc_macro2::Span::call_site());
+ quote! {
+ #idx_lit => <#disp_ident as ::mingling::Dispatcher<Self::Enum>>::begin(
+ &#disp_ident::default(),
+ __args,
+ ),
+ }
+ })
+ .collect();
+
+ let max_words_lit = proc_macro2::Literal::usize_unsuffixed(max_words);
+ let seed0_lit = proc_macro2::Literal::u64_unsuffixed(seed0);
+ let seed1_lit = proc_macro2::Literal::u64_unsuffixed(seed1);
+ let b_lit = proc_macro2::Literal::usize_unsuffixed(b);
+ let m_lit = proc_macro2::Literal::usize_unsuffixed(keys.len());
+
+ quote! {
+ fn dispatch_args(
+ raw: &[String],
+ ) -> Result<::mingling::AnyOutput<Self::Enum>, ::mingling::error::ProgramInternalExecuteError>
+ {
+ const __DISPATCH_PHF_MAX_WORDS: usize = #max_words_lit;
+ const __DISPATCH_PHF_SEED0: u64 = #seed0_lit;
+ const __DISPATCH_PHF_SEED1: u64 = #seed1_lit;
+ const __DISPATCH_PHF_BUCKETS: usize = #b_lit;
+ const __DISPATCH_PHF_MOD: usize = #m_lit;
+ static __DISPATCH_PHF_KEYS: &[&str] = &[#(#name_lits),*];
+ static __DISPATCH_PHF_DISP: &[u64] = &[#(#disp_lits),*];
+
+ #[inline(always)]
+ fn __dispatch_phf_hash(s: &str, seed: u64) -> u64 {
+ let mut h = 0xcbf29ce484222325u64 ^ seed;
+ for &b in s.as_bytes() {
+ h ^= b as u64;
+ h = h.wrapping_mul(0x100000001b3);
+ }
+ h ^= h >> 33;
+ h = h.wrapping_mul(0xff51afd7ed558ccd);
+ h ^= h >> 33;
+ h = h.wrapping_mul(0xc4ceb9fe1a85ec53);
+ h ^= h >> 33;
+ h
+ }
+
+ let raw_string = format!("{} ", raw.join(" "));
+ let raw_str = raw_string.as_str();
+ let bytes = raw_str.as_bytes();
+
+ // Record the byte offset of the space that terminates each word
+ // (`raw_str` always ends with a trailing space).
+ let mut __ends = [0usize; #max_words_lit];
+ let mut __w = 0usize;
+ let mut __i = 0usize;
+ while __w < __DISPATCH_PHF_MAX_WORDS && __i < bytes.len() {
+ if bytes[__i] == b' ' {
+ __ends[__w] = __i;
+ __w += 1;
+ }
+ __i += 1;
+ }
+
+ // Scan the input from the longest possible prefix down to the
+ // first word, so the longest registered prefix wins (mirroring
+ // the linear list and trie generators).
+ let mut __k = __w;
+ while __k > 0 {
+ let __cand = &raw_str[..__ends[__k - 1]];
+ let __h1 = (__dispatch_phf_hash(__cand, __DISPATCH_PHF_SEED1)
+ % __DISPATCH_PHF_BUCKETS as u64) as usize;
+ let __d = __DISPATCH_PHF_DISP[__h1];
+ let __idx = ((__dispatch_phf_hash(__cand, __DISPATCH_PHF_SEED0)
+ % __DISPATCH_PHF_MOD as u64)
+ + __d)
+ % __DISPATCH_PHF_MOD as u64;
+ let __idx = __idx as usize;
+ if __DISPATCH_PHF_KEYS[__idx].as_bytes() == __cand.as_bytes() {
+ let __args: Vec<String> = raw.iter().skip(__k).cloned().collect();
+ let __cp = match __idx {
+ #(#arms)*
+ _ => unreachable!("dispatch_phf: perfect-hash index out of range"),
+ };
+ return match __cp {
+ ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0),
+ ::mingling::ChainProcess::Err(chain_process_error) => {
+ Err(chain_process_error.into())
+ }
+ };
+ }
+ __k -= 1;
+ }
+
+ 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 2b264f7..08fc78b 100644
--- a/mingling_macros/src/systems/dispatch_tree_gen.rs
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -1,57 +1,122 @@
// Doc Not Optimize
+//! Char-level trie dispatch generator (`dispatch_tree` feature).
+//!
+//! 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.
+//!
+//! The "longest registered prefix" fallback (try the exact endpoint at this
+//! node, then its parent, …) is **not** inlined into every arm. Instead each
+//! trie node gets an id and every arm *calls* a single generic
+//! `__trie_fallback<G>` method that runs that node's exact-endpoint checks
+//! and tail-recurses to the parent, returning `None` when nothing in the
+//! chain matches (the caller then produces the no-match result). This keeps
+//! the generated code linear in the table size: inlining the whole fallback
+//! chain per arm grew quadratically with nesting depth (a 1024×16 nested
+//! table emitted ~13 MB of tokens).
+//!
+//! The generator returns two token streams: the `dispatch_args` method (for
+//! the `ProgramCollect` trait impl) and the `__trie_fallback` method (for an
+//! inherent impl of the program type). The fallback uses the concrete `Self`
+//! (the program type implements `ProgramCollect` with `Enum = Self`, and the
+//! bench harness's `BenchDispatch` mirrors that), so it lives outside the
+//! trait.
+
use std::collections::BTreeMap;
use proc_macro2::TokenStream;
use quote::quote;
-/// 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.
-pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> TokenStream {
- let nodes: Vec<(String, String)> = entries
- .iter()
- .map(|(name, disp, _)| (name.replace('.', " "), disp.clone()))
- .collect();
+/// A trie node recorded for the shared longest-prefix fallback: its
+/// exact-endpoint checks and its parent's node id.
+struct FallbackNode {
+ node_id: usize,
+ parent: Option<usize>,
+ exact_checks: Vec<TokenStream>,
+}
- let dispatch_body = build_dispatch_body(
- &nodes,
- 0,
- &quote! {
- return Ok(Self::build_entry_fallback(raw.to_vec()));
- },
- );
+/// Emit a `starts_with` dispatch arm.
+///
+/// `group_ty` is the type the dispatchers are generic over: `Self::Enum`
+/// inside `dispatch_args` (trait impl) or `Self` inside the fallback
+/// (inherent impl). `wrap_some` selects `return Some(match __cp …)`
+/// (fallback) vs `return match __cp …` (dispatch).
+fn make_starts_with_arm(
+ name: &str,
+ disp_type: &str,
+ group_ty: &TokenStream,
+ wrap_some: bool,
+) -> TokenStream {
+ 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();
+ let ret = if wrap_some {
+ quote! {
+ return Some(match __cp {
+ ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0),
+ ::mingling::ChainProcess::Err(chain_process_error) => {
+ Err(chain_process_error.into())
+ }
+ });
+ }
+ } else {
+ quote! {
+ return match __cp {
+ ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0),
+ ::mingling::ChainProcess::Err(chain_process_error) => {
+ Err(chain_process_error.into())
+ }
+ };
+ }
+ };
+ 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<#group_ty>>::begin(
+ &#disp_ident::default(),
+ trimmed_args,
+ );
+ #ret
+ }
+ }
+}
+/// Call site of the shared fallback from inside `dispatch_args`: if the
+/// fallback chain resolved an exact endpoint, return it; otherwise fall
+/// through (the terminal no-match result follows the root match).
+fn fallback_call(node_id: usize) -> TokenStream {
+ let id_lit = proc_macro2::Literal::usize_unsuffixed(node_id);
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();
- let mut raw_chars = raw_str.chars();
- #dispatch_body
+ if let Some(__r) = Self::__trie_fallback(raw, raw_str, #id_lit) {
+ return __r;
}
}
}
/// Recursively build the trie match body.
///
-/// `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.
+/// `nodes`: slice of (`display_name`, `disp_type`) for commands that share the
+/// same prefix so far. `depth`: the character index currently being matched.
+/// `node_id` / `parent_id`: trie node identity used by the shared
+/// longest-prefix fallback; `fallbacks` collects one entry per interior node
+/// for the `__trie_fallback` method emitted by the caller.
///
/// Matching follows the same "longest registered prefix" rule used by the
-/// dynamic dispatcher: a child (longer) path is preferred over an exact
+/// other generators: 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.
+/// the exact endpoint here dispatched (via the fallback).
fn build_dispatch_body(
nodes: &[(String, String)],
depth: usize,
- no_match: &TokenStream,
+ node_id: usize,
+ parent_id: Option<usize>,
+ next_id: &mut usize,
+ fallbacks: &mut Vec<FallbackNode>,
) -> TokenStream {
if nodes.is_empty() {
- return no_match.clone();
+ return parent_id.map_or_else(|| quote! {}, fallback_call);
}
let mut groups: BTreeMap<char, Vec<(String, String)>> = BTreeMap::new();
@@ -68,43 +133,21 @@ fn build_dispatch_body(
}
}
- let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream {
- 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())
- }
- };
- }
- }
- };
-
- // 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)* }
- };
+ // Register this node in the fallback table (interior nodes only — leaf
+ // exact checks run inline in the walk). The fallback's exact checks use
+ // the concrete `Self` (see the module docs) and wrap results in `Some`.
+ if !groups.is_empty() {
+ fallbacks.push(FallbackNode {
+ node_id,
+ parent: parent_id,
+ exact_checks: exact_nodes
+ .iter()
+ .map(|(name, disp_type)| make_starts_with_arm(name, disp_type, &quote!(Self), true))
+ .collect(),
+ });
+ }
+ let self_enum = quote!(Self::Enum);
let mut arms = Vec::new();
for (&ch, sub_nodes) in &groups {
@@ -112,17 +155,27 @@ fn build_dispatch_body(
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.
+ let arm = make_starts_with_arm(name, disp_type, &self_enum, false);
+ let fb = fallback_call(node_id);
+ // Try the child first; if it does not match, defer to this node's
+ // longest-prefix fallback so the longer path wins when present.
arms.push(quote! {
Some(#ch_char) => {
#arm
- #level_no_match
+ #fb
}
});
} else {
- let sub_body = build_dispatch_body(sub_nodes, depth + 1, &level_no_match);
+ let child_id = *next_id;
+ *next_id += 1;
+ let sub_body = build_dispatch_body(
+ sub_nodes,
+ depth + 1,
+ child_id,
+ Some(node_id),
+ next_id,
+ fallbacks,
+ );
arms.push(quote! {
Some(#ch_char) => {
#sub_body
@@ -132,18 +185,101 @@ fn build_dispatch_body(
}
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());
+ // No children exist for this node; only the exact endpoint(s) apply,
+ // then defer to the parent's fallback (longest-prefix semantics).
+ let mut body = exact_nodes
+ .iter()
+ .map(|(name, disp_type)| make_starts_with_arm(name, disp_type, &self_enum, false))
+ .collect::<Vec<TokenStream>>();
+ if let Some(p) = parent_id {
+ body.push(fallback_call(p));
+ }
quote! { #(#body)* }
} else {
+ let fb = fallback_call(node_id);
quote! {
match raw_chars.nth(0) {
#(#arms)*
_ => {
- #level_no_match
+ #fb
}
}
}
}
}
+
+/// Generate the `dispatch_args()` method (for the `ProgramCollect` trait
+/// impl) plus the `__trie_fallback` method (for an inherent impl of the
+/// program type). Returns `(dispatch_method, extra_inherent_items)`.
+pub(crate) fn gen_dispatch_args_trie(
+ entries: &[(String, String, String)],
+) -> (TokenStream, TokenStream) {
+ let nodes: Vec<(String, String)> = entries
+ .iter()
+ .map(|(name, disp, _)| (name.replace('.', " "), disp.clone()))
+ .collect();
+
+ let mut next_id = 1usize;
+ let mut fallbacks: Vec<FallbackNode> = Vec::new();
+ let dispatch_body = build_dispatch_body(&nodes, 0, 0, None, &mut next_id, &mut fallbacks);
+
+ let fallback_arms: Vec<TokenStream> = fallbacks
+ .iter()
+ .map(|fb| {
+ let id_lit = proc_macro2::Literal::usize_unsuffixed(fb.node_id);
+ let exact = &fb.exact_checks;
+ let tail = fb.parent.map_or_else(
+ || quote! { None },
+ |p| {
+ let p_lit = proc_macro2::Literal::usize_unsuffixed(p);
+ quote! { Self::__trie_fallback(raw, raw_str, #p_lit) }
+ },
+ );
+ quote! {
+ #id_lit => {
+ #(#exact)*
+ #tail
+ }
+ }
+ })
+ .collect();
+
+ let dispatch_fn = 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();
+ let mut raw_chars = raw_str.chars();
+ #dispatch_body
+ Ok(Self::build_entry_fallback(raw.to_vec()))
+ }
+ };
+
+ let fallback_fn = quote! {
+ /// Longest-prefix fallback: run the exact-endpoint checks of trie
+ /// node `__node`, then defer to its parent (tail-recursively), or
+ /// return `None` at the root when nothing matched. Shared by all
+ /// arms instead of being inlined into each one, keeping the generated
+ /// code linear in the table size. Lives in an inherent impl, where
+ /// `Self` is the program type (`ProgramCollect`'s `Enum = Self`).
+ #[allow(dead_code)]
+ #[inline(never)]
+ fn __trie_fallback(
+ raw: &[String],
+ raw_str: &str,
+ __node: usize,
+ ) -> Option<Result<
+ ::mingling::AnyOutput<Self>,
+ ::mingling::error::ProgramInternalExecuteError,
+ >> {
+ match __node {
+ #(#fallback_arms)*
+ _ => None,
+ }
+ }
+ };
+
+ (dispatch_fn, fallback_fn)
+}