aboutsummaryrefslogtreecommitdiff
path: root/arg_picker_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'arg_picker_macros/src')
-rw-r--r--arg_picker_macros/src/arg.rs205
-rw-r--r--arg_picker_macros/src/internal_repeat.rs315
-rw-r--r--arg_picker_macros/src/lib.rs32
3 files changed, 552 insertions, 0 deletions
diff --git a/arg_picker_macros/src/arg.rs b/arg_picker_macros/src/arg.rs
new file mode 100644
index 0000000..55860c4
--- /dev/null
+++ b/arg_picker_macros/src/arg.rs
@@ -0,0 +1,205 @@
+use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::TokenStream as TS2;
+use quote::quote;
+
+pub(crate) fn arg(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let args = split_at_commas(&tokens);
+ if args.is_empty() {
+ return quote! { compile_error!("arg! flaguires 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!("arg! only supports at most one short name") }.into();
+ }
+
+ // Extract short char and string aliases
+ let short_char: Option<char> = rest
+ .iter()
+ .find(|a| is_char_literal(a))
+ .and_then(|a| extract_char(a));
+ let aliases: Vec<String> = rest
+ .iter()
+ .filter(|a| is_string_literal(a))
+ .filter_map(|a| extract_string(a))
+ .collect();
+
+ // Parse first argument
+ // arg![name: Type] → name, with explicit type
+ // arg![Type] → positional type, no name
+ let colon_pos = first
+ .iter()
+ .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':'));
+ let has_named_colon = colon_pos.is_some_and(|pos| pos > 0 && !is_double_colon(first, pos));
+
+ let first_slice = first.as_slice();
+ let (name, ty, is_named) = if has_named_colon {
+ let pos = colon_pos.unwrap();
+ // `name : Type`
+ (
+ Some(&first_slice[..pos]),
+ Some(&first_slice[pos + 1..]),
+ true,
+ )
+ } else {
+ // No `name:` prefix → the entire first argument is the type
+ (None, Some(first_slice), false)
+ };
+
+ // Build full names
+ let full_names: Vec<String> = match name {
+ Some(n) => {
+ let mut v = vec![join_idents(n)];
+ v.extend(aliases);
+ v
+ }
+ None => aliases,
+ };
+
+ let path: TS2 = {
+ let ty_ts: TS2 = ty
+ .map(|t| {
+ let ts: TokenStream = t.iter().cloned().collect();
+ TS2::from(ts)
+ })
+ .unwrap_or(TS2::new());
+
+ #[cfg(feature = "mingling_support")]
+ let import = quote! { ::mingling::picker::PickerArg };
+
+ #[cfg(not(feature = "mingling_support"))]
+ let import = quote! { ::arg_picker::PickerArg };
+
+ if ty.is_some() {
+ quote! { #import::<#ty_ts> }
+ } else {
+ quote! { #import::<_> }
+ }
+ };
+
+ // full: &["name", "alias", ...] or &[]
+ let full_value: TS2 = if !full_names.is_empty() {
+ let strs: Vec<proc_macro2::Literal> = full_names
+ .iter()
+ .map(|s| proc_macro2::Literal::string(s))
+ .collect();
+ quote! { &[#(#strs),*] }
+ } else {
+ quote! { &[] }
+ };
+
+ // short: Some('c') or None
+ let short_value: TS2 = match short_char {
+ Some(c) => {
+ let lit = proc_macro2::Literal::character(c);
+ quote! { ::std::option::Option::Some(#lit) }
+ }
+ None => quote! { ::std::option::Option::None },
+ };
+
+ let positional_value: bool = !is_named;
+
+ let result = quote! {
+ #path {
+ full: #full_value,
+ short: #short_value,
+ positional: #positional_value,
+ internal_type: ::std::marker::PhantomData,
+ }
+ };
+
+ result.into()
+}
+
+fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
+ 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_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 extract_char(tokens: &[TokenTree]) -> Option<char> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ let cs: Vec<char> = s.chars().collect();
+ if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' {
+ let inner: String = cs[1..cs.len() - 1].iter().collect();
+ // inner is the character between the quotes of a char literal.
+ // For a literal `'n'`, inner is `"n"` (the character n).
+ // For an escape `'\n'`, inner is the actual newline character.
+ // The catch-all handles both literal single chars and escape
+ // sequences (the escaped char IS the actual control character).
+ match inner.as_str() {
+ "\\\\" => Some('\\'),
+ "\\'" => Some('\''),
+ _ => inner.chars().next(),
+ }
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn extract_string(tokens: &[TokenTree]) -> Option<String> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ let cs: Vec<char> = 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()
+}
diff --git a/arg_picker_macros/src/internal_repeat.rs b/arg_picker_macros/src/internal_repeat.rs
new file mode 100644
index 0000000..4b2242b
--- /dev/null
+++ b/arg_picker_macros/src/internal_repeat.rs
@@ -0,0 +1,315 @@
+use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree};
+
+pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let (range_start, range_end, body_start) = parse_range(&tokens);
+
+ let mut body: Vec<TokenTree> = tokens[body_start..].to_vec();
+ if body.len() == 1
+ && let TokenTree::Group(g) = &body[0]
+ && 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, range_start, range_end));
+ }
+ 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()
+ .is_some_and(|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::<Vec<_>>()
+ .join("")
+ .replace(' ', "");
+
+ s.parse().unwrap_or(12)
+}
+
+/// Walk tokens, replacing:
+/// `$` → current
+/// `^$` → max
+/// `$^` → min
+/// `$+` → current + 1 (clamped)
+/// `$-` → current - 1 (clamped)
+/// `ident$` → ident{current}
+/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups.
+fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec<TokenTree> {
+ 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, current, min, max) {
+ let (items, consumed) = exp;
+ out.extend(items);
+ i += consumed;
+ continue;
+ }
+
+ // `^$` — max value
+ if let TokenTree::Punct(p) = &tokens[i]
+ && p.as_char() == '^'
+ && i + 1 < tokens.len()
+ && let TokenTree::Punct(p2) = &tokens[i + 1]
+ && p2.as_char() == '$'
+ {
+ out.push(TokenTree::Literal(Literal::usize_suffixed(max)));
+ i += 2;
+ continue;
+ }
+
+ // `ident$` / `ident$+` / `ident$-` / `ident$^`
+ // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min}
+ if let TokenTree::Ident(id) = &tokens[i] {
+ if i + 1 < tokens.len()
+ && let TokenTree::Punct(p) = &tokens[i + 1]
+ && p.as_char() == '$'
+ {
+ // Check for ident$+ / ident$- / ident$^
+ if i + 2 < tokens.len() {
+ match &tokens[i + 2] {
+ TokenTree::Punct(p2) if p2.as_char() == '+' => {
+ // ident$+ → {ident}{current+1}
+ let name = format!("{}{}", id, current + 1);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '-' => {
+ // ident$- → {ident}{current-1}
+ let val = current.saturating_sub(1);
+ let name = format!("{}{}", id, val);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '^' => {
+ let name = format!("{}{}", id, min);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ _ => {}
+ }
+ }
+ // ident$ alone → {ident}{current}
+ let name = format!("{}{}", id, current);
+ 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() == '$' => {
+ // lookahead for $^, $+, $-
+ if i + 1 < tokens.len() {
+ match &tokens[i + 1] {
+ TokenTree::Punct(p2) if p2.as_char() == '^' => {
+ // $^ → min
+ out.push(TokenTree::Literal(Literal::usize_suffixed(min)));
+ i += 2;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '+' => {
+ // $+ → current + 1
+ out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1)));
+ i += 2;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '-' => {
+ // $- → current - 1
+ let val = current.saturating_sub(1);
+ out.push(TokenTree::Literal(Literal::usize_suffixed(val)));
+ i += 2;
+ continue;
+ }
+ _ => {}
+ }
+ }
+ // `$` alone → current
+ out.push(TokenTree::Literal(Literal::usize_suffixed(current)));
+ i += 1;
+ continue;
+ }
+ TokenTree::Group(g) => {
+ let inner = expand_body_vec(&g.stream(), current, min, max);
+ 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, current: usize, min: usize, max: usize) -> Vec<TokenTree> {
+ let v: Vec<TokenTree> = stream.clone().into_iter().collect();
+ expand_body(&v, current, min, max)
+}
+
+/// Try to expand a repetition group.
+///
+/// New syntax (repetition marker `+` is the LAST token INSIDE the parens):
+/// - `(group,+)` — repeat `*` times (current), `,` is the separator
+/// - `(group,+)` — repeat `*` times, `,` is the separator
+/// - `(group,+)` — repeat `*` times, `;` is the separator
+/// - `(group +)` — repeat `*` times, no separator
+/// - `(group,+)+` — repeat `*+1` times (with separator)
+/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used]
+///
+/// The `*` is the current counter value. An optional `+` or `-` immediately
+/// after the closing paren shifts the repeat count up or down by one.
+fn try_expand_paren_group(
+ tokens: &[TokenTree],
+ i: usize,
+ current: usize,
+ _min: usize,
+ _max: usize,
+) -> Option<(Vec<TokenTree>, usize)> {
+ let group = match tokens.get(i)? {
+ TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g,
+ _ => return None,
+ };
+
+ let stream: Vec<TokenTree> = group.stream().into_iter().collect();
+
+ // Repetition syntax: the LAST token inside the parens MUST be `+`.
+ // `(content,+)` — repeat * times, `,` separator
+ // `(content;+)` — repeat * times, `;` separator
+ // `(content,+)` — repeat * times, no separator
+ // `(content,+)+` — repeat *+1 times
+ // `(content,+)-` — repeat *-1 times
+ // An optional `+` / `-` right after `)` shifts the count by ±1.
+ //
+ // Any `+` tokens inside `content` are treated as regular Rust syntax
+ // (trait bounds, etc.) — the repetition marker is ONLY the final `+`.
+
+ let last_is_plus = stream
+ .last()
+ .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+'));
+ if !last_is_plus {
+ return None;
+ }
+
+ // Determine separator and inner content.
+ let (inner, sep_str): (Vec<TokenTree>, &str) = {
+ if stream.len() >= 2 {
+ let sep_idx = stream.len() - 2;
+ match &stream[sep_idx] {
+ TokenTree::Punct(p) if p.as_char() == ',' => {
+ // (...,content,+) — inner is everything before the last `,`
+ let content: Vec<TokenTree> = stream[..sep_idx].into();
+ (content, ",")
+ }
+ TokenTree::Punct(p) if p.as_char() == ';' => {
+ let content: Vec<TokenTree> = stream[..sep_idx].into();
+ (content, ";")
+ }
+ _ => {
+ // (content+) — no separator, the `+` is the only special token
+ let content: Vec<TokenTree> = stream[..stream.len() - 1].into();
+ (content, "")
+ }
+ }
+ } else {
+ // (+) — bare repetition marker, empty content
+ (vec![], "")
+ }
+ };
+
+ // Handle modifier after `)`
+ let rest = &tokens[i + 1..];
+ let modifier: isize = match rest.first() {
+ Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1,
+ Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1,
+ _ => 0,
+ };
+ let consumed = if modifier != 0 { 2 } else { 1 };
+ let repeat_count = (current as isize + modifier) as usize;
+
+ let mut out = Vec::new();
+ for n in 1..=repeat_count {
+ if n > 1 && !sep_str.is_empty() {
+ out.push(TokenTree::Punct(proc_macro::Punct::new(
+ sep_str.chars().next().unwrap(),
+ proc_macro::Spacing::Alone,
+ )));
+ }
+ out.extend(expand_body(&inner, n, 1, repeat_count));
+ }
+
+ Some((out, consumed))
+}
diff --git a/arg_picker_macros/src/lib.rs b/arg_picker_macros/src/lib.rs
new file mode 100644
index 0000000..18cce84
--- /dev/null
+++ b/arg_picker_macros/src/lib.rs
@@ -0,0 +1,32 @@
+#![doc = include_str!("../README.md")]
+
+use proc_macro::TokenStream;
+
+mod arg;
+mod internal_repeat;
+
+/// 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 `PickerArg`.
+///
+/// # Syntax
+///
+/// ```ignore
+/// use arg_picker_macros::flag;
+///
+/// let basic = arg![name: String];
+/// let with_short_name = arg![name: String, 'n'];
+/// let with_short_alias = arg![name: String, 'n', "alias"];
+/// let positional = arg![String];
+/// let positional_with_name = arg![String, 'n', "alias"];
+/// ```
+#[proc_macro]
+pub fn arg(input: TokenStream) -> TokenStream {
+ arg::arg(input)
+}