From 8b016a54db2555c087053e8d3cc94a1cad8b8f1f Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 00:06:00 +0200 Subject: [PATCH 01/22] Free of syn --- Cargo.toml | 3 +- src/lib.rs | 311 ++++++++++++++++++++++++++++------------------------- 2 files changed, 163 insertions(+), 151 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45cf373..addefe7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,8 @@ alloc = [] proc-macro = true [dependencies] -syn = "2.0" quote = "1.0" -proc-macro2 = "1.0" +unsynn = "0.3" [dev-dependencies] static_assertions = "1.1" diff --git a/src/lib.rs b/src/lib.rs index 5fc030c..18311b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,15 +5,23 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg, rustdoc_internals))] use proc_macro::TokenStream; -use proc_macro2::{Literal, Span as Span2, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, - spanned::Spanned as _, - token::Comma, - Attribute, Error, Ident, LitInt, LitStr, Result, -}; +use quote::quote; +use unsynn::{format_ident, TokenStream as TokenStream2, *}; + +unsynn! { + // `#[doc(fake_variadic)]` + struct FakeVariadicAttr { + _hash: Pound, // # + _bracket: BracketGroupContaining::, + } + + // `doc(fake_variadic)` + struct FakeVariadicInner { + _doc: Ident, // doc + _paren: ParenthesisGroupContaining::, // (fake_variadic) + } +} + struct AllTuples { fake_variadic: bool, macro_ident: Ident, @@ -22,24 +30,60 @@ struct AllTuples { idents: Vec, } -impl Parse for AllTuples { - fn parse(input: ParseStream) -> Result { - let fake_variadic = input.call(parse_fake_variadic_attr)?; - let macro_ident = input.parse::()?; - input.parse::()?; - let start = input.parse::()?.base10_parse()?; - input.parse::()?; - let end_span = input.span(); - let end = input.parse::()?.base10_parse()?; - input.parse::()?; +impl Parser for AllTuples { + fn parser(tokens: &mut TokenIter) -> Result { + // Optional leading `#[doc(fake_variadic)]` + let fake_variadic = tokens + .transaction(|t| FakeVariadicAttr::parser(t)) + .map(|attr| { + // Validate that the ident inside the parens is "fake_variadic" + // and the outer ident is "doc". + // FakeVariadicInner already encodes the structure; we just need + // to confirm the ident values at runtime. + let inner_tokens = attr._bracket.content; + inner_tokens._doc.to_string() == "doc" + && inner_tokens._paren.content.to_string() == "fake_variadic" + }) + .unwrap_or(false); + + // macro_ident + let macro_ident_tok = Ident::parser(tokens)?; + let macro_ident = Ident::new(¯o_ident_tok.to_string(), Span::call_site()); + + // `,` + Comma::parser(tokens)?; + + // start + let start_tok = LiteralInteger::parser(tokens)?; + let start_tt = start_tok.to_token_iter().next(); + let start: usize = start_tok.value().try_into().map_err(|_| { + Error::other::(start_tt, tokens, "start out of range".into()).unwrap_err() + })?; + + // `,` + Comma::parser(tokens)?; + + // end + let end_tok = LiteralInteger::parser(tokens)?; + let end_tt = end_tok.to_token_iter().next(); + let end: usize = end_tok.value().try_into().map_err(|_| { + Error::other::(end_tt.clone(), tokens, "end out of range".into()).unwrap_err() + })?; if end < start { - return Err(Error::new(end_span, "`start` should <= `end`")); + return Error::other(end_tt, tokens, "`start` should <= `end`".into()); } - let mut idents = vec![input.parse::()?]; - while input.parse::().is_ok() { - idents.push(input.parse::()?); + // `,` + Comma::parser(tokens)?; + + // one or more idents separated by commas + let first_tok = Ident::parser(tokens)?; + let mut idents = vec![Ident::new(&first_tok.to_string(), Span::call_site())]; + + while tokens.transaction(|t| Comma::parser(t)).is_ok() { + let tok = Ident::parser(tokens)?; + idents.push(Ident::new(&tok.to_string(), Span::call_site())); } Ok(AllTuples { @@ -169,36 +213,21 @@ impl Parse for AllTuples { /// ``` #[proc_macro] pub fn all_tuples(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as AllTuples); - let ident_tuples = (0..input.end) - .map(|i| { - let idents = input - .idents - .iter() - .map(|ident| format_ident!("{}{}", ident, i)); - to_ident_tuple(idents, input.idents.len()) - }) - .collect::>(); + let input = match parse_all_tuples(input) { + Ok(input) => input, + Err(err) => { + let msg = err.to_string(); + return TokenStream::from(quote! { compile_error!(#msg) }); + } + }; + let ident_tuples = build_ident_tuples(&input); let macro_ident = &input.macro_ident; - let invocations = (input.start..=input.end) - .chain(if input.fake_variadic && input.start > 1 { - // chain n = 1 - vec![1] - } else { - vec![] - }) - .map(|n| { - let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n); - let attrs = attrs(&input, n); - quote! { - #macro_ident!(#attrs #ident_tuples); - } - }); - TokenStream::from(quote! { - #( - #invocations - )* - }) + let invocations = make_invocation_range(&input).map(|n| { + let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n); + let attrs = attrs(&input, n); + quote! { #macro_ident!(#attrs #ident_tuples); } + }); + TokenStream::from(quote! { #(#invocations)* }) } /// A variant of [`all_tuples!`] that enumerates its output. @@ -256,35 +285,21 @@ pub fn all_tuples(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn all_tuples_enumerated(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as AllTuples); - let ident_tuples = (0..input.end) - .map(|i| { - let idents = input - .idents - .iter() - .map(|ident| format_ident!("{}{}", ident, i)); - to_ident_tuple_enumerated(idents, i) - }) - .collect::>(); + let input = match parse_all_tuples(input) { + Ok(input) => input, + Err(err) => { + let msg = err.to_string(); + return TokenStream::from(quote! { compile_error!(#msg) }); + } + }; + let ident_tuples = build_ident_tuples_enumerated(&input); let macro_ident = &input.macro_ident; - let invocations = (input.start..=input.end) - .chain(if input.fake_variadic && input.start > 1 { - vec![1] - } else { - vec![] - }) - .map(|n| { - let ident_tuples = choose_ident_tuples_enumerated(&input, &ident_tuples, n); - let attrs = attrs(&input, n); - quote! { - #macro_ident!(#attrs #ident_tuples); - } - }); - TokenStream::from(quote! { - #( - #invocations - )* - }) + let invocations = make_invocation_range(&input).map(|n| { + let ident_tuples = choose_ident_tuples_enumerated(&input, &ident_tuples, n); + let attrs = attrs(&input, n); + quote! { #macro_ident!(#attrs #ident_tuples); } + }); + TokenStream::from(quote! { #(#invocations)* }) } /// Helper macro to generate tuple pyramids with their length. Useful to generate scaffolding to @@ -408,8 +423,31 @@ pub fn all_tuples_enumerated(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as AllTuples); - let ident_tuples = (0..input.end) + let input = match parse_all_tuples(input) { + Ok(input) => input, + Err(err) => { + let msg = err.to_string(); + return TokenStream::from(quote! { compile_error!(#msg) }); + } + }; + let ident_tuples = build_ident_tuples(&input); + let macro_ident = &input.macro_ident; + let invocations = make_invocation_range(&input).map(|n| { + let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n); + let attrs = attrs(&input, n); + quote! { #macro_ident!(#n, #attrs #ident_tuples); } + }); + TokenStream::from(quote! { #(#invocations)* }) +} + +fn parse_all_tuples(input: TokenStream) -> Result { + let ts: TokenStream2 = input.into(); + let mut iter = ts.to_token_iter(); + AllTuples::parser(&mut iter) +} + +fn build_ident_tuples(input: &AllTuples) -> Vec { + (0..input.end) .map(|i| { let idents = input .idents @@ -417,52 +455,30 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { .map(|ident| format_ident!("{}{}", ident, i)); to_ident_tuple(idents, input.idents.len()) }) - .collect::>(); - let macro_ident = &input.macro_ident; - let invocations = (input.start..=input.end) - .chain(if input.fake_variadic && input.start > 1 { - vec![1] - } else { - vec![] + .collect() +} + +fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec { + (0..input.end) + .map(|i| { + let idents = input + .idents + .iter() + .map(|ident| format_ident!("{}{}", ident, i)); + to_ident_tuple_enumerated(idents, i) }) - .map(|n| { - let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n); - let attrs = attrs(&input, n); - quote! { - #macro_ident!(#n, #attrs #ident_tuples); - } - }); - TokenStream::from(quote! { - #( - #invocations - )* - }) + .collect() } -/// Parses the attribute `#[doc(fake_variadic)]` -fn parse_fake_variadic_attr(input: ParseStream) -> Result { - let attribute = match input.call(Attribute::parse_outer)? { - attributes if attributes.is_empty() => return Ok(false), - attributes if attributes.len() == 1 => attributes[0].clone(), - attributes => { - return Err(Error::new( - input.span(), - format!("Expected exactly one attribute, got {}", attributes.len()), - )) - } +/// Returns an iterator over the invocation arities, including the optional fake-variadic `n=1`. +fn make_invocation_range(input: &AllTuples) -> impl Iterator { + let base = input.start..=input.end; + let extra: Vec = if input.fake_variadic && input.start > 1 { + vec![1] + } else { + vec![] }; - - if attribute.path().is_ident("doc") { - let nested = attribute.parse_args::()?; - if nested == "fake_variadic" { - return Ok(true); - } - } - - Err(Error::new( - attribute.meta.span(), - "Unexpected attribute".to_string(), - )) + base.chain(extra) } fn choose_ident_tuples(input: &AllTuples, ident_tuples: &[TokenStream2], n: usize) -> TokenStream2 { @@ -520,31 +536,28 @@ fn attrs(input: &AllTuples, n: usize) -> TokenStream2 { let cfg = quote! { any(docsrs, docsrs_dep) }; // The `#[doc(fake_variadic)]` attr has to be on the first impl block. if n == 1 { - let doc = LitStr::new( - &format!( - "This trait is implemented for tuple{s1} {range} item{s2} long.", - range = if input.start == input.end { - format!("exactly {}", input.start) - } else { - format!( - "{down}up to {up}", - down = if input.start != 0 { - format!("down to {} ", input.start) - } else { - "".to_string() - }, - up = input.end - ) - }, - s1 = if input.end > input.start { "s" } else { "" }, - s2 = if input.end >= input.start && input.end > 1 { - "s" - } else { - "" - } - ), - Span2::call_site(), - ); + let doc = Literal::string(&format!( + "This trait is implemented for tuple{s1} {range} item{s2} long.", + range = if input.start == input.end { + format!("exactly {}", input.start) + } else { + format!( + "{down}up to {up}", + down = if input.start != 0 { + format!("down to {} ", input.start) + } else { + "".to_string() + }, + up = input.end + ) + }, + s1 = if input.end > input.start { "s" } else { "" }, + s2 = if input.end >= input.start && input.end > 1 { + "s" + } else { + "" + } + )); if input.start <= 1 && input.end >= 1 { // n == 1 and it's included quote! { From 2a53c02a760a640eb048120f76d705b3165fab30 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 00:13:37 +0200 Subject: [PATCH 02/22] Clippy --- src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 18311b3..aead843 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ // FIXME(15321): solve CI failures, then replace with `#![expect()]`. #![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")] -#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg, rustdoc_internals))] +#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg))] use proc_macro::TokenStream; use quote::quote; @@ -41,8 +41,7 @@ impl Parser for AllTuples { // FakeVariadicInner already encodes the structure; we just need // to confirm the ident values at runtime. let inner_tokens = attr._bracket.content; - inner_tokens._doc.to_string() == "doc" - && inner_tokens._paren.content.to_string() == "fake_variadic" + inner_tokens._doc == "doc" && inner_tokens._paren.content == "fake_variadic" }) .unwrap_or(false); From 5f78d3651a35a3a0ac58bde61a8fde7274fa1b3c Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:06:37 +0200 Subject: [PATCH 03/22] Remove some hand parsing --- src/lib.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index aead843..141c786 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,9 @@ use quote::quote; use unsynn::{format_ident, TokenStream as TokenStream2, *}; unsynn! { + keyword KDoc = "doc"; + keyword KFakeVariadic = "fake_variadic"; + // `#[doc(fake_variadic)]` struct FakeVariadicAttr { _hash: Pound, // # @@ -17,8 +20,8 @@ unsynn! { // `doc(fake_variadic)` struct FakeVariadicInner { - _doc: Ident, // doc - _paren: ParenthesisGroupContaining::, // (fake_variadic) + _doc: KDoc, // doc + _paren: ParenthesisGroupContaining::, // (fake_variadic) } } @@ -33,17 +36,7 @@ struct AllTuples { impl Parser for AllTuples { fn parser(tokens: &mut TokenIter) -> Result { // Optional leading `#[doc(fake_variadic)]` - let fake_variadic = tokens - .transaction(|t| FakeVariadicAttr::parser(t)) - .map(|attr| { - // Validate that the ident inside the parens is "fake_variadic" - // and the outer ident is "doc". - // FakeVariadicInner already encodes the structure; we just need - // to confirm the ident values at runtime. - let inner_tokens = attr._bracket.content; - inner_tokens._doc == "doc" && inner_tokens._paren.content == "fake_variadic" - }) - .unwrap_or(false); + let fake_variadic = FakeVariadicAttr::parse(tokens).is_ok(); // macro_ident let macro_ident_tok = Ident::parser(tokens)?; From 8103d92f9b5704c081b62b5a1244b16112ea85ba Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:16:50 +0200 Subject: [PATCH 04/22] Remove manual parsing --- src/lib.rs | 92 +++++++++++++----------------------------------------- 1 file changed, 22 insertions(+), 70 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 141c786..edb76a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,68 +23,16 @@ unsynn! { _doc: KDoc, // doc _paren: ParenthesisGroupContaining::, // (fake_variadic) } -} - -struct AllTuples { - fake_variadic: bool, - macro_ident: Ident, - start: usize, - end: usize, - idents: Vec, -} - -impl Parser for AllTuples { - fn parser(tokens: &mut TokenIter) -> Result { - // Optional leading `#[doc(fake_variadic)]` - let fake_variadic = FakeVariadicAttr::parse(tokens).is_ok(); - - // macro_ident - let macro_ident_tok = Ident::parser(tokens)?; - let macro_ident = Ident::new(¯o_ident_tok.to_string(), Span::call_site()); - - // `,` - Comma::parser(tokens)?; - - // start - let start_tok = LiteralInteger::parser(tokens)?; - let start_tt = start_tok.to_token_iter().next(); - let start: usize = start_tok.value().try_into().map_err(|_| { - Error::other::(start_tt, tokens, "start out of range".into()).unwrap_err() - })?; - - // `,` - Comma::parser(tokens)?; - // end - let end_tok = LiteralInteger::parser(tokens)?; - let end_tt = end_tok.to_token_iter().next(); - let end: usize = end_tok.value().try_into().map_err(|_| { - Error::other::(end_tt.clone(), tokens, "end out of range".into()).unwrap_err() - })?; - - if end < start { - return Error::other(end_tt, tokens, "`start` should <= `end`".into()); - } - - // `,` - Comma::parser(tokens)?; - - // one or more idents separated by commas - let first_tok = Ident::parser(tokens)?; - let mut idents = vec![Ident::new(&first_tok.to_string(), Span::call_site())]; - - while tokens.transaction(|t| Comma::parser(t)).is_ok() { - let tok = Ident::parser(tokens)?; - idents.push(Ident::new(&tok.to_string(), Span::call_site())); - } - - Ok(AllTuples { - fake_variadic, - macro_ident, - start, - end, - idents, - }) + struct AllTuples { + fake_variadic: Option, + macro_ident: Ident, + _comma1: Comma, + start: usize, + _comma2: Comma, + end: usize, + _comma3: Comma, + idents: CommaDelimitedVec, } } @@ -435,7 +383,7 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { fn parse_all_tuples(input: TokenStream) -> Result { let ts: TokenStream2 = input.into(); let mut iter = ts.to_token_iter(); - AllTuples::parser(&mut iter) + AllTuples::parse(&mut iter) } fn build_ident_tuples(input: &AllTuples) -> Vec { @@ -444,7 +392,7 @@ fn build_ident_tuples(input: &AllTuples) -> Vec { let idents = input .idents .iter() - .map(|ident| format_ident!("{}{}", ident, i)); + .map(|ident| format_ident!("{}{}", ident.value, i)); to_ident_tuple(idents, input.idents.len()) }) .collect() @@ -456,7 +404,7 @@ fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec { let idents = input .idents .iter() - .map(|ident| format_ident!("{}{}", ident, i)); + .map(|ident| format_ident!("{}{}", ident.value, i)); to_ident_tuple_enumerated(idents, i) }) .collect() @@ -465,7 +413,7 @@ fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec { /// Returns an iterator over the invocation arities, including the optional fake-variadic `n=1`. fn make_invocation_range(input: &AllTuples) -> impl Iterator { let base = input.start..=input.end; - let extra: Vec = if input.fake_variadic && input.start > 1 { + let extra: Vec = if input.fake_variadic.is_some() && input.start > 1 { vec![1] } else { vec![] @@ -478,8 +426,11 @@ fn choose_ident_tuples(input: &AllTuples, ident_tuples: &[TokenStream2], n: usiz // idents with subscript numbers e.g. (F₁, F₂, …, Fₙ). // We don't want two numbers, so we use the // original, unnumbered idents for this case. - if input.fake_variadic && n == 1 { - let ident_tuple = to_ident_tuple(input.idents.iter().cloned(), input.idents.len()); + if input.fake_variadic.is_some() && n == 1 { + let ident_tuple = to_ident_tuple( + input.idents.iter().map(|ident| &ident.value).cloned(), + input.idents.len(), + ); quote! { #ident_tuple } } else { let ident_tuples = &ident_tuples[..n]; @@ -492,8 +443,9 @@ fn choose_ident_tuples_enumerated( ident_tuples: &[TokenStream2], n: usize, ) -> TokenStream2 { - if input.fake_variadic && n == 1 { - let ident_tuple = to_ident_tuple_enumerated(input.idents.iter().cloned(), 0); + if input.fake_variadic.is_some() && n == 1 { + let ident_tuple = + to_ident_tuple_enumerated(input.idents.iter().map(|ident| &ident.value).cloned(), 0); quote! { #ident_tuple } } else { let ident_tuples = &ident_tuples[..n]; @@ -517,7 +469,7 @@ fn to_ident_tuple_enumerated(idents: impl Iterator, idx: usize) -> /// n: number of elements fn attrs(input: &AllTuples, n: usize) -> TokenStream2 { - if !input.fake_variadic { + if !input.fake_variadic.is_some() { return TokenStream2::default(); } match n { From af0c2efbcb3f7968491ce4eb38e34cd6f0f99fc5 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:20:34 +0200 Subject: [PATCH 05/22] Add validation back in --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index edb76a4..a6fa4eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -383,7 +383,11 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { fn parse_all_tuples(input: TokenStream) -> Result { let ts: TokenStream2 = input.into(); let mut iter = ts.to_token_iter(); - AllTuples::parse(&mut iter) + let tuples = AllTuples::parse(&mut iter)?; + if tuples.end < tuples.start { + return Error::other(None, &iter, "`start` should <= `end`".into()); + } + Ok(tuples) } fn build_ident_tuples(input: &AllTuples) -> Vec { From c2a41f41340b1a2490d6cebfe29112959e888d3a Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:22:56 +0200 Subject: [PATCH 06/22] Improve comment --- src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a6fa4eb..0d4a5a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,16 +14,17 @@ unsynn! { // `#[doc(fake_variadic)]` struct FakeVariadicAttr { - _hash: Pound, // # + _hash: Pound, _bracket: BracketGroupContaining::, } // `doc(fake_variadic)` struct FakeVariadicInner { - _doc: KDoc, // doc - _paren: ParenthesisGroupContaining::, // (fake_variadic) + _doc: KDoc, + _paren: ParenthesisGroupContaining::, } + // `all_tuples!(#[doc(fake_variadic)] some_macro, 1, 16, P, Q, ..)` struct AllTuples { fake_variadic: Option, macro_ident: Ident, From 649da91e0962adb829cdb4f5e6dcbb06359ce80d Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:28:31 +0200 Subject: [PATCH 07/22] Improve parser --- src/lib.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0d4a5a0..ced7858 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,18 +12,6 @@ unsynn! { keyword KDoc = "doc"; keyword KFakeVariadic = "fake_variadic"; - // `#[doc(fake_variadic)]` - struct FakeVariadicAttr { - _hash: Pound, - _bracket: BracketGroupContaining::, - } - - // `doc(fake_variadic)` - struct FakeVariadicInner { - _doc: KDoc, - _paren: ParenthesisGroupContaining::, - } - // `all_tuples!(#[doc(fake_variadic)] some_macro, 1, 16, P, Q, ..)` struct AllTuples { fake_variadic: Option, @@ -35,6 +23,12 @@ unsynn! { _comma3: Comma, idents: CommaDelimitedVec, } + + // `#[doc(fake_variadic)]` + struct FakeVariadicAttr { + _hash: Pound, + _bracket: BracketGroupContaining::<(KDoc, ParenthesisGroupContaining::)>, + } } /// Helper macro to generate tuple pyramids. Useful to generate scaffolding to work around Rust @@ -474,7 +468,7 @@ fn to_ident_tuple_enumerated(idents: impl Iterator, idx: usize) -> /// n: number of elements fn attrs(input: &AllTuples, n: usize) -> TokenStream2 { - if !input.fake_variadic.is_some() { + if input.fake_variadic.is_none() { return TokenStream2::default(); } match n { From b36a53d44141c63ea422b6cf4a6b497c1851a6d1 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:29:35 +0200 Subject: [PATCH 08/22] Improve error --- src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ced7858..1ba9af3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -380,7 +380,14 @@ fn parse_all_tuples(input: TokenStream) -> Result { let mut iter = ts.to_token_iter(); let tuples = AllTuples::parse(&mut iter)?; if tuples.end < tuples.start { - return Error::other(None, &iter, "`start` should <= `end`".into()); + return Error::other( + None, + &iter, + format!( + "`start` should <= `end`, but got {} > {}", + tuples.start, tuples.end + ), + ); } Ok(tuples) } From a81b90a7075a8293daa862c2db22cc873dbd56a5 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:44:32 +0200 Subject: [PATCH 09/22] Allow clippy --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1ba9af3..c42a191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,10 @@ // FIXME(15321): solve CI failures, then replace with `#![expect()]`. #![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg))] +#![expect( + clippy::result_large_err, + reason = "The error variant intentionally holds detailed diagnostic information." +)] use proc_macro::TokenStream; use quote::quote; From 01b2de79d9ea2003e6930ab7ab6048c915c92989 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:47:15 +0200 Subject: [PATCH 10/22] Bump msrv --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index addefe7..65493a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://github.com/bevyengine/variadics_please" repository = "https://github.com/bevyengine/variadics_please" license = "MIT OR Apache-2.0" keywords = ["bevy", "variadics", "docs"] -rust-version = "1.81.0" +rust-version = "1.83.0" categories = ["rust-patterns"] exclude = ["tools/", ".github/"] documentation = "https://docs.rs/variadics_please" From 9bc828e6b416320966081c3379d345b970d60a5d Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:48:12 +0200 Subject: [PATCH 11/22] Allow unicode license --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 1d72763..16a2d80 100644 --- a/deny.toml +++ b/deny.toml @@ -20,6 +20,7 @@ allow = [ "MIT-0", "Unlicense", "Zlib", + "Unicode-3.0", ] exceptions = [ From 21312e92cbf5b9834d369bed402002924aaba145 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:49:26 +0200 Subject: [PATCH 12/22] Deny syn --- deny.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deny.toml b/deny.toml index 16a2d80..afbbb64 100644 --- a/deny.toml +++ b/deny.toml @@ -74,6 +74,8 @@ deny = [ { name = "android-activity", deny-multiple-versions = true }, { name = "glam", deny-multiple-versions = true }, { name = "raw-window-handle", deny-multiple-versions = true }, + # keep syn entirely out of tree, since we use unsynn instead + { name = "syn" } ] [sources] From 56a666fc9698fb601d3b9beffedb479b98334fbc Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 01:52:55 +0200 Subject: [PATCH 13/22] Lint --- deny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index afbbb64..33cc849 100644 --- a/deny.toml +++ b/deny.toml @@ -75,7 +75,7 @@ deny = [ { name = "glam", deny-multiple-versions = true }, { name = "raw-window-handle", deny-multiple-versions = true }, # keep syn entirely out of tree, since we use unsynn instead - { name = "syn" } + { name = "syn" }, ] [sources] From 3bbf5e1d9b34e6935e0bfa29dcf1f55a38e9c076 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:03:29 +0200 Subject: [PATCH 14/22] Add `at` field --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c42a191..176a0eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -384,8 +384,9 @@ fn parse_all_tuples(input: TokenStream) -> Result { let mut iter = ts.to_token_iter(); let tuples = AllTuples::parse(&mut iter)?; if tuples.end < tuples.start { + let at = tuples.macro_ident.to_token_iter().next(); return Error::other( - None, + at, &iter, format!( "`start` should <= `end`, but got {} > {}", From 42bb6de2af4a73b409b44907287e493ecd035051 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:40:38 +0200 Subject: [PATCH 15/22] Improve diagnostics --- src/lib.rs | 110 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 176a0eb..820b3e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,13 +17,13 @@ unsynn! { keyword KFakeVariadic = "fake_variadic"; // `all_tuples!(#[doc(fake_variadic)] some_macro, 1, 16, P, Q, ..)` - struct AllTuples { + struct AllTuplesParsed { fake_variadic: Option, macro_ident: Ident, _comma1: Comma, - start: usize, + start: LiteralInteger, _comma2: Comma, - end: usize, + end: LiteralInteger, _comma3: Comma, idents: CommaDelimitedVec, } @@ -35,6 +35,14 @@ unsynn! { } } +struct AllTuples { + fake_variadic: bool, + macro_ident: Ident, + start: usize, + end: usize, + idents: Vec, +} + /// Helper macro to generate tuple pyramids. Useful to generate scaffolding to work around Rust /// lacking variadics. Invoking `all_tuples!(impl_foo, start, end, P, Q, ..)` /// invokes `impl_foo` providing ident tuples through arity `start..end`. @@ -155,8 +163,7 @@ pub fn all_tuples(input: TokenStream) -> TokenStream { let input = match parse_all_tuples(input) { Ok(input) => input, Err(err) => { - let msg = err.to_string(); - return TokenStream::from(quote! { compile_error!(#msg) }); + return err; } }; let ident_tuples = build_ident_tuples(&input); @@ -227,8 +234,7 @@ pub fn all_tuples_enumerated(input: TokenStream) -> TokenStream { let input = match parse_all_tuples(input) { Ok(input) => input, Err(err) => { - let msg = err.to_string(); - return TokenStream::from(quote! { compile_error!(#msg) }); + return err; } }; let ident_tuples = build_ident_tuples_enumerated(&input); @@ -365,8 +371,7 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { let input = match parse_all_tuples(input) { Ok(input) => input, Err(err) => { - let msg = err.to_string(); - return TokenStream::from(quote! { compile_error!(#msg) }); + return err; } }; let ident_tuples = build_ident_tuples(&input); @@ -379,22 +384,63 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { TokenStream::from(quote! { #(#invocations)* }) } -fn parse_all_tuples(input: TokenStream) -> Result { +fn parse_all_tuples(input: TokenStream) -> std::result::Result { let ts: TokenStream2 = input.into(); let mut iter = ts.to_token_iter(); - let tuples = AllTuples::parse(&mut iter)?; - if tuples.end < tuples.start { - let at = tuples.macro_ident.to_token_iter().next(); - return Error::other( - at, - &iter, - format!( - "`start` should <= `end`, but got {} > {}", - tuples.start, tuples.end - ), - ); + let tuples = AllTuplesParsed::parse(&mut iter).map_err(pretty_print_error)?; + let start: usize = match tuples.start.value().try_into() { + Ok(start) => start, + Err(_) => { + return Err(span_error( + tuples.start, + "`start` should be in the range of 0..usize::MAX", + )); + } + }; + let end: usize = match tuples.end.value().try_into() { + Ok(end) => end, + Err(_) => { + return Err(span_error( + tuples.end, + "`end` should be in the range of 0..usize::MAX", + )); + } + }; + if end < start { + return Err(span_error(tuples.start, "`start` should <= `end`")); } - Ok(tuples) + Ok(AllTuples { + fake_variadic: tuples.fake_variadic.is_some(), + macro_ident: tuples.macro_ident, + start, + end, + idents: tuples.idents.iter().map(|i| i.value.clone()).collect(), + }) +} + +fn pretty_print_error(err: Error) -> TokenStream { + let span = err + .failed_at() + .map(|tt| tt.span()) + .unwrap_or_else(Span::call_site); + + let msg = match err.kind { + ErrorKind::Other { reason } => reason, + _ => err.to_string(), + }; + let ts = TokenStream2::from(quote::quote_spanned! { span => compile_error!(#msg); }); + return ts.into(); +} + +fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream { + let span = tokens + .to_token_iter() + .next() + .map(|tt| tt.span()) + .unwrap_or_else(Span::call_site); + let msg = format!("expected {}", msg); + let ts = TokenStream2::from(quote::quote_spanned! { span => compile_error!(#msg); }); + return ts.into(); } fn build_ident_tuples(input: &AllTuples) -> Vec { @@ -403,7 +449,7 @@ fn build_ident_tuples(input: &AllTuples) -> Vec { let idents = input .idents .iter() - .map(|ident| format_ident!("{}{}", ident.value, i)); + .map(|ident| format_ident!("{}{}", ident, i)); to_ident_tuple(idents, input.idents.len()) }) .collect() @@ -415,7 +461,7 @@ fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec { let idents = input .idents .iter() - .map(|ident| format_ident!("{}{}", ident.value, i)); + .map(|ident| format_ident!("{}{}", ident, i)); to_ident_tuple_enumerated(idents, i) }) .collect() @@ -424,7 +470,7 @@ fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec { /// Returns an iterator over the invocation arities, including the optional fake-variadic `n=1`. fn make_invocation_range(input: &AllTuples) -> impl Iterator { let base = input.start..=input.end; - let extra: Vec = if input.fake_variadic.is_some() && input.start > 1 { + let extra: Vec = if input.fake_variadic && input.start > 1 { vec![1] } else { vec![] @@ -437,11 +483,8 @@ fn choose_ident_tuples(input: &AllTuples, ident_tuples: &[TokenStream2], n: usiz // idents with subscript numbers e.g. (F₁, F₂, …, Fₙ). // We don't want two numbers, so we use the // original, unnumbered idents for this case. - if input.fake_variadic.is_some() && n == 1 { - let ident_tuple = to_ident_tuple( - input.idents.iter().map(|ident| &ident.value).cloned(), - input.idents.len(), - ); + if input.fake_variadic && n == 1 { + let ident_tuple = to_ident_tuple(input.idents.iter().cloned(), input.idents.len()); quote! { #ident_tuple } } else { let ident_tuples = &ident_tuples[..n]; @@ -454,9 +497,8 @@ fn choose_ident_tuples_enumerated( ident_tuples: &[TokenStream2], n: usize, ) -> TokenStream2 { - if input.fake_variadic.is_some() && n == 1 { - let ident_tuple = - to_ident_tuple_enumerated(input.idents.iter().map(|ident| &ident.value).cloned(), 0); + if input.fake_variadic && n == 1 { + let ident_tuple = to_ident_tuple_enumerated(input.idents.iter().cloned(), 0); quote! { #ident_tuple } } else { let ident_tuples = &ident_tuples[..n]; @@ -480,7 +522,7 @@ fn to_ident_tuple_enumerated(idents: impl Iterator, idx: usize) -> /// n: number of elements fn attrs(input: &AllTuples, n: usize) -> TokenStream2 { - if input.fake_variadic.is_none() { + if !input.fake_variadic { return TokenStream2::default(); } match n { From 90b2d94c72ce280936edf7dcafc73423a27e1eab Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:44:46 +0200 Subject: [PATCH 16/22] Improve diagnostics --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 820b3e1..6514699 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -426,6 +426,7 @@ fn pretty_print_error(err: Error) -> TokenStream { let msg = match err.kind { ErrorKind::Other { reason } => reason, + ErrorKind::UnexpectedToken => format!("expected {}", err.expected_type_name()), _ => err.to_string(), }; let ts = TokenStream2::from(quote::quote_spanned! { span => compile_error!(#msg); }); From 269aa0445f75eb3e1fec763e91d85debddf1856a Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:52:06 +0200 Subject: [PATCH 17/22] Lint --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6514699..5e7d4a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -429,8 +429,8 @@ fn pretty_print_error(err: Error) -> TokenStream { ErrorKind::UnexpectedToken => format!("expected {}", err.expected_type_name()), _ => err.to_string(), }; - let ts = TokenStream2::from(quote::quote_spanned! { span => compile_error!(#msg); }); - return ts.into(); + let ts = quote::quote_spanned! { span => compile_error!(#msg); }; + ts.into() } fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream { @@ -440,8 +440,8 @@ fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream { .map(|tt| tt.span()) .unwrap_or_else(Span::call_site); let msg = format!("expected {}", msg); - let ts = TokenStream2::from(quote::quote_spanned! { span => compile_error!(#msg); }); - return ts.into(); + let ts = quote::quote_spanned! { span => compile_error!(#msg); }; + ts.into() } fn build_ident_tuples(input: &AllTuples) -> Vec { From 00a95564661cb0f183b61ac25a38865523d53740 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:53:06 +0200 Subject: [PATCH 18/22] Shrimple --- src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5e7d4a1..96b7d0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -429,8 +429,7 @@ fn pretty_print_error(err: Error) -> TokenStream { ErrorKind::UnexpectedToken => format!("expected {}", err.expected_type_name()), _ => err.to_string(), }; - let ts = quote::quote_spanned! { span => compile_error!(#msg); }; - ts.into() + quote::quote_spanned! { span => compile_error!(#msg); }.into() } fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream { @@ -439,9 +438,7 @@ fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream { .next() .map(|tt| tt.span()) .unwrap_or_else(Span::call_site); - let msg = format!("expected {}", msg); - let ts = quote::quote_spanned! { span => compile_error!(#msg); }; - ts.into() + quote::quote_spanned! { span => compile_error!(#msg); }.into() } fn build_ident_tuples(input: &AllTuples) -> Vec { From 83eca7da3468199efbb2f51a9cbef78ed9ca62e7 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 02:59:53 +0200 Subject: [PATCH 19/22] Comments --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 96b7d0d..fc36a8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ unsynn! { keyword KDoc = "doc"; keyword KFakeVariadic = "fake_variadic"; - // `all_tuples!(#[doc(fake_variadic)] some_macro, 1, 16, P, Q, ..)` + /// `all_tuples!(#[doc(fake_variadic)] some_macro, 1, 16, P, Q, ..)` struct AllTuplesParsed { fake_variadic: Option, macro_ident: Ident, @@ -28,13 +28,14 @@ unsynn! { idents: CommaDelimitedVec, } - // `#[doc(fake_variadic)]` + /// `#[doc(fake_variadic)]` struct FakeVariadicAttr { _hash: Pound, _bracket: BracketGroupContaining::<(KDoc, ParenthesisGroupContaining::)>, } } +/// Duplication of [`AllTuplesParsed`], but after it went through validation. struct AllTuples { fake_variadic: bool, macro_ident: Ident, From 05af7c6f84b25ee23941a4294536e2ee2e6361a5 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 03:02:07 +0200 Subject: [PATCH 20/22] Update src/lib.rs Co-authored-by: atlv --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index fc36a8b..94f619d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -408,7 +408,7 @@ fn parse_all_tuples(input: TokenStream) -> std::result::Result Date: Thu, 30 Apr 2026 03:33:40 +0200 Subject: [PATCH 21/22] Feedback --- RELEASES.md | 11 +++++++++++ src/lib.rs | 3 +++ 2 files changed, 14 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index d3d20dd..2fcf01a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,16 @@ # `variadics_please` Release Notes +## Unrealeased + +- Switch from `syn` to `unsynn` + - [`syn` is known to be a major compile time bottleneck](https://fasterthanli.me/articles/the-virtue-of-unsynn). To improve the situation for users of `variadics_please`, we switched to `unsynn`, which is the alternative used by [`facet`](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust). + - The compile time speedup depends on your local setup and the complexity of the macro invocation, + but on one test setup, the cold compilation time went from about 2.16 seconds to 0.56 seconds. + - The code generated by `variadics_please` should be identical to before. + Our Error messages may look a bit different now, but they should be just as readable. + If you encounter any weird behavior or diagnostics, let us know. + - Using `unsynn` bumps the MSRV from 1.81.0 to 1.83.0 + ## Version 1.1 - added `all_tuples_enumerated`, which provides the index of each item in the tuple diff --git a/src/lib.rs b/src/lib.rs index 94f619d..60e484d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ // FIXME(15321): solve CI failures, then replace with `#![expect()]`. #![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg))] +// This lint is triggered from inside the `unsynn!` macro, so we are forced to suppress it for the entire module. #![expect( clippy::result_large_err, reason = "The error variant intentionally holds detailed diagnostic information." @@ -419,6 +420,8 @@ fn parse_all_tuples(input: TokenStream) -> std::result::Result fn pretty_print_error(err: Error) -> TokenStream { let span = err .failed_at() From 9dc660b2b7f13869ca9da16fcc9838425ecfd3f6 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Thu, 30 Apr 2026 04:25:31 +0200 Subject: [PATCH 22/22] lint --- RELEASES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 2fcf01a..1c6ee81 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,15 +1,15 @@ # `variadics_please` Release Notes -## Unrealeased +## Unreleased - Switch from `syn` to `unsynn` - - [`syn` is known to be a major compile time bottleneck](https://fasterthanli.me/articles/the-virtue-of-unsynn). To improve the situation for users of `variadics_please`, we switched to `unsynn`, which is the alternative used by [`facet`](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust). + - [`syn` is known to be a major compile time bottleneck](https://fasterthanli.me/articles/the-virtue-of-unsynn). To improve the situation for users of `variadics_please`, we switched to `unsynn`, which is the alternative used by [`facet`](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust). - The compile time speedup depends on your local setup and the complexity of the macro invocation, but on one test setup, the cold compilation time went from about 2.16 seconds to 0.56 seconds. - - The code generated by `variadics_please` should be identical to before. + - The code generated by `variadics_please` should be identical to before. Our Error messages may look a bit different now, but they should be just as readable. If you encounter any weird behavior or diagnostics, let us know. - - Using `unsynn` bumps the MSRV from 1.81.0 to 1.83.0 + - Using `unsynn` bumps the MSRV from 1.81.0 to 1.83.0 ## Version 1.1