From 4665fc94ab9508d115298dd988e2381354f46c01 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 14 Jul 2026 01:36:27 +0800 Subject: feat(picker): add core trait, types, and builder macro for argument parsing Implement the foundational `mingling_picker` library along with its companion `mingling_picker_macros` crate. The picker provides: - `Pickable` trait for parsing types from raw strings - `PickerResult` enum modeling parse outcomes - `Picker` struct for storing and indexing command-line arguments - `PickerRequirement` struct for declarative parameter definitions - `PickerPattern` family (1..32) of typed pattern structs via the `internal_repeat` proc macro - `req!` proc macro as a succinct builder for `PickerRequirement` Re-export `mingling_picker::macros::*` from the `mingling` crate when the `picker` feature is enabled, replacing the previous wildcard re-export of `mingling_macros`. --- mingling_picker_macros/src/internal_repeat.rs | 204 ++++++++++++++++++++++++ mingling_picker_macros/src/lib.rs | 29 ++++ mingling_picker_macros/src/req.rs | 214 ++++++++++++++++++++++++++ 3 files changed, 447 insertions(+) create mode 100644 mingling_picker_macros/src/internal_repeat.rs create mode 100644 mingling_picker_macros/src/req.rs (limited to 'mingling_picker_macros/src') diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs new file mode 100644 index 0000000..9feedfd --- /dev/null +++ b/mingling_picker_macros/src/internal_repeat.rs @@ -0,0 +1,204 @@ +use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; + +pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream { + let tokens: Vec = input.into_iter().collect(); + let (range_start, range_end, body_start) = parse_range(&tokens); + + let mut body: Vec = tokens[body_start..].to_vec(); + if body.len() == 1 { + if let TokenTree::Group(g) = &body[0] { + if g.delimiter() == Delimiter::Brace { + body = g.stream().into_iter().collect(); + } + } + } + + let mut result = Vec::new(); + for i in range_start..=range_end { + result.extend(expand_body(&body, i)); + } + result.into_iter().collect() +} + +/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat). +/// Returns `(start, end_inclusive, body_start_index)`. +fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) { + // Find => separator + let arrow_pos = tokens.windows(2).position(|w| { + matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=') + && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>') + }); + + let (arrow_pos, body_start) = match arrow_pos { + Some(p) => (p, p + 2), + None => return (1, 12, 0), // fallback + }; + + let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect(); + + // Try to find `..` or `..=` pattern + // `..` is two Punct('.') tokens + let dotdot = before.windows(2).position(|w| { + matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.') + && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.') + }); + + if let Some(dd) = dotdot { + // Start value: tokens before `..` + let start = parse_usize_tokens(&before[..dd]); + let after_dd = &before[dd + 2..]; + + // Check for `..=` (inclusive range) + let (inclusive, end_tokens) = if after_dd.first().map_or( + false, + |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='), + ) { + (true, &after_dd[1..]) + } else { + (false, after_dd) + }; + + let end = parse_usize_tokens(end_tokens); + + if inclusive { + (start, end, body_start) + } else { + // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1 + if end > start { + (start, end - 1, body_start) + } else { + (1, 12, body_start) // fallback + } + } + } else { + // No `..` found — fallback to simple count + let count = parse_usize_tokens(&before); + (1, count, body_start) + } +} + +/// Parse a sequence of tokens as a single usize value. +fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize { + let s: String = tokens + .iter() + .map(|t| match t { + TokenTree::Literal(l) => l.to_string(), + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect::>() + .join("") + .replace(' ', ""); + + s.parse().unwrap_or(12) +} + +/// Walk tokens, replacing `$` in identifier tails, and expanding +/// `( … )+` / `( … ,)+` / `( … ;)+` groups. +fn expand_body(tokens: &[TokenTree], outer: usize) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < tokens.len() { + // Check for a parenthesized repetition group: ( ... ) sep? + + if let Some(exp) = try_expand_paren_group(tokens, i, outer) { + let (items, consumed) = exp; + out.extend(items); + i += consumed; + continue; + } + + // Identifier followed by `$` → combined ident + number + if let TokenTree::Ident(id) = &tokens[i] { + if i + 1 < tokens.len() { + if let TokenTree::Punct(p) = &tokens[i + 1] { + if p.as_char() == '$' { + let name = format!("{}{outer}", id.to_string()); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 2; + continue; + } + } + } + out.push(tokens[i].clone()); + i += 1; + continue; + } + + match &tokens[i] { + TokenTree::Punct(p) if p.as_char() == '$' => { + out.push(TokenTree::Literal(Literal::usize_suffixed(outer))); + } + TokenTree::Group(g) => { + let inner = expand_body_vec(&g.stream(), outer); + out.push(TokenTree::Group(Group::new( + g.delimiter(), + inner.into_iter().collect(), + ))); + } + other => out.push(other.clone()), + } + i += 1; + } + out +} + +fn expand_body_vec(stream: &TokenStream, outer: usize) -> Vec { + let v: Vec = stream.clone().into_iter().collect(); + expand_body(&v, outer) +} + +/// Try to expand `( … )+` / `( … ,)+` / `( … ;)+` at position `i`. +/// +/// The `+` is AFTER the closing paren. An optional separator (`,` or `;`) +/// may appear between `)` and `+`. +/// Returns `(expanded_tokens, consumed_count)` or `None`. +fn try_expand_paren_group( + tokens: &[TokenTree], + i: usize, + outer: usize, +) -> Option<(Vec, usize)> { + let group = match tokens.get(i)? { + TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g, + _ => return None, + }; + + // Check tokens AFTER the group for `+` (optionally preceded by `,` or `;`) + let rest = &tokens[i + 1..]; + let sep: Option<&str> = match rest.first() { + Some(TokenTree::Punct(p)) if p.as_char() == ',' => { + if matches!(rest.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '+') { + Some(",") + } else { + return None; + } + } + Some(TokenTree::Punct(p)) if p.as_char() == ';' => { + if matches!(rest.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '+') { + Some(";") + } else { + return None; + } + } + Some(TokenTree::Punct(p)) if p.as_char() == '+' => None, + _ => return None, + }; + + let consumed = if sep.is_some() { 3 } else { 2 }; // group + sep? + + + + let inner: Vec = group.stream().into_iter().collect(); + + let mut out = Vec::new(); + for n in 1..=outer { + if n > 1 { + if let Some(s) = sep { + out.push(TokenTree::Punct(proc_macro::Punct::new( + s.chars().next().unwrap(), + proc_macro::Spacing::Alone, + ))); + } + } + out.extend(expand_body(&inner, n)); + } + + Some((out, consumed)) +} diff --git a/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs index 8b13789..a3c93eb 100644 --- a/mingling_picker_macros/src/lib.rs +++ b/mingling_picker_macros/src/lib.rs @@ -1 +1,30 @@ +use proc_macro::TokenStream; +mod internal_repeat; +mod req; + +/// Core proc-macro: repeats a template body `count` times. +/// +/// Internal call signature: `internal_repeat!(count => { template })` +#[proc_macro] +pub fn internal_repeat(input: TokenStream) -> TokenStream { + internal_repeat::internal_repeat(input) +} + +/// Quick builder for `PickerRequirement`. +/// +/// # Syntax +/// +/// ```ignore +/// use mingling_picker_macros::req; +/// +/// let basic = req![name: String]; +/// let with_short_name = req![name: String, 'n']; +/// let with_short_alias = req![name: String, 'n', "alias"]; +/// let positional = req![String]; +/// let positional_with_name = req![String, 'n', "alias"]; +/// ``` +#[proc_macro] +pub fn req(input: TokenStream) -> TokenStream { + req::req(input) +} diff --git a/mingling_picker_macros/src/req.rs b/mingling_picker_macros/src/req.rs new file mode 100644 index 0000000..7e652fa --- /dev/null +++ b/mingling_picker_macros/src/req.rs @@ -0,0 +1,214 @@ +use proc_macro::{TokenStream, TokenTree}; +use proc_macro2::TokenStream as TS2; +use quote::quote; + +pub(crate) fn req(input: TokenStream) -> TokenStream { + let tokens: Vec = input.into_iter().collect(); + let args = split_at_commas(&tokens); + if args.is_empty() { + return quote! { compile_error!("req! requires at least one argument") }.into(); + } + + let first = &args[0]; + let rest = &args[1..]; + + // Validate: at most one char literal + let char_count = rest.iter().filter(|a| is_char_literal(a)).count(); + if char_count > 1 { + return quote! { compile_error!("req! only supports at most one short name") }.into(); + } + + // Extract short char and string aliases + let short_char: Option = rest + .iter() + .find(|a| is_char_literal(a)) + .and_then(|a| extract_char(a)); + let aliases: Vec = rest + .iter() + .filter(|a| is_string_literal(a)) + .filter_map(|a| extract_string(a)) + .collect(); + + // Parse first argument + let colon_pos = first + .iter() + .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':')); + let has_generics = first + .iter() + .any(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '<')); + + let first_slice = first.as_slice(); + let (name, ty, is_named) = match colon_pos { + Some(pos) if pos > 0 && !is_double_colon(first, pos) => { + // `name : Type` + ( + Some(&first_slice[..pos]), + Some(&first_slice[pos + 1..]), + true, + ) + } + _ => { + if has_generics || !is_single_ident(first) || !has_char_or_string(rest) { + // Type path, generic type, or bare type (no more args) + (None, Some(first_slice), false) + } else { + // Single ident followed by char/string → bare name + (Some(first_slice), None, true) + } + } + }; + + // Build full names + let full_names: Vec = match name { + Some(n) => { + let mut v = vec![join_idents(n)]; + v.extend(aliases); + v + } + None => aliases, + }; + + // Generate code + let import = quote! { ::mingling::picker::PickerRequirement }; + + // Type parameter + let ty_ts: TS2 = ty + .map(|t| { + let ts: TokenStream = t.iter().cloned().collect(); + ts.to_string().parse().unwrap() + }) + .unwrap_or(TS2::new()); + + let with_type = if ty.is_some() { + quote! { #import::<#ty_ts> } + } else { + quote! { #import::<_> } + }; + + // .with_full(...) + let with_full = if !full_names.is_empty() { + let strs: Vec = full_names + .iter() + .map(|s| proc_macro2::Literal::string(s)) + .collect(); + quote! { .with_full(&[#(#strs),*]) } + } else { + TS2::new() + }; + + // .with_short(...) + let with_short = short_char + .map(|c| { + let lit = proc_macro2::Literal::character(c); + quote! { .with_short(#lit) } + }) + .unwrap_or(TS2::new()); + + // .with_positional(...) + let pos = !is_named; + + let result = quote! { + #with_type::default() + #with_full + #with_short + .with_positional(#pos) + }; + + result.into() +} + +fn split_at_commas(tokens: &[TokenTree]) -> Vec> { + let mut result = vec![Vec::new()]; + let mut depth = 0u32; + for t in tokens { + match t { + TokenTree::Group(_g) => { + depth += 1; + result.last_mut().unwrap().push(t.clone()); + depth -= 1; + } + TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => { + result.push(Vec::new()); + } + _ => result.last_mut().unwrap().push(t.clone()), + } + } + result +} + +fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool { + pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':') +} + +fn is_single_ident(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 && matches!(&tokens[0], TokenTree::Ident(_)) +} + +fn is_char_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + let s = l.to_string(); + s.starts_with('\'') && s.len() >= 3 + }) +} + +fn is_string_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + l.to_string().starts_with('"') + }) +} + +fn has_char_or_string(args: &[Vec]) -> bool { + args.iter() + .any(|a| is_char_literal(a) || is_string_literal(a)) +} + +fn extract_char(tokens: &[TokenTree]) -> Option { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec = s.chars().collect(); + if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' { + let inner: String = cs[1..cs.len() - 1].iter().collect(); + match inner.as_str() { + "n" => Some('\n'), + "t" => Some('\t'), + "r" => Some('\r'), + "0" => Some('\0'), + "\\\\" => Some('\\'), + "\\'" => Some('\''), + _ => inner.chars().next(), + } + } else { + None + } + } + _ => None, + } +} + +fn extract_string(tokens: &[TokenTree]) -> Option { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec = s.chars().collect(); + if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' { + Some(cs[1..cs.len() - 1].iter().collect()) + } else { + None + } + } + _ => None, + } +} + +fn join_idents(tokens: &[TokenTree]) -> String { + tokens + .iter() + .map(|t| match t { + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect() +} -- cgit