From f4d72486f991bc60ad4771edc51f26b0c1d2d53d Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 25 Jul 2026 14:36:47 +0800 Subject: [PATCH] feat: support row attributes Signed-off-by: tison --- README.md | 22 +++++++++++ macroweave/src/expand.rs | 6 ++- macroweave/src/parse.rs | 77 +++++++++++++++++++++++++++++++++------ macroweave/tests/tests.rs | 51 ++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5498369..1cce38d 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,33 @@ Placeholders are substituted only inside the splice body, and the surrounding to `#( ..., )*` and `#( ... ),*` are different: the latter does not produce a trailing comma. This matches delimiter repetition in `macro_rules!`. +# Row attributes + +Rows may be prefixed with outer attributes. The attributes are emitted immediately before that row's generated tokens: + +```rust +use macroweave::repeat; + +repeat!((Name, Value) in [ + (ALWAYS, 1usize), + + #[cfg(any())] + (DISABLED, compile_error!("disabled row was emitted")), +] { + const Name: usize = Value; +}); + +assert_eq!(ALWAYS, 1); +``` + +This is useful for conditional compilation because `macroweave` only moves the attribute into the expanded output; Rust still evaluates the `cfg` predicate. + # Syntax notes - Bind placeholders as bare identifiers, such as `Ty` or `Name`. - Tuple rows bind multiple placeholders, and `_` skips a row value. - Row values can contain one or more Rust tokens. Top-level commas separate rows. +- Rows can start with one or more outer attributes, such as `#[cfg(...)]`. - Nested invocations are supported. Use different placeholder names at each level. diff --git a/macroweave/src/expand.rs b/macroweave/src/expand.rs index 6805c21..c1cbbec 100644 --- a/macroweave/src/expand.rs +++ b/macroweave/src/expand.rs @@ -31,9 +31,10 @@ pub fn repeat(input: TokenStream) -> Result { let mut output = TokenStream::new(); for row in &invocation.table.rows { + output.extend(row.attrs.clone()); output.extend(substitute_tokens( &invocation.table.names, - row, + &row.values, invocation.body.clone(), )); } @@ -100,9 +101,10 @@ fn expand_splices( output.extend([separator.clone()]); } } + output.extend(row.attrs.clone()); output.extend(substitute_tokens( &table.names, - row, + &row.values, splice.template.clone(), )); } diff --git a/macroweave/src/parse.rs b/macroweave/src/parse.rs index 150a25a..8642cbe 100644 --- a/macroweave/src/parse.rs +++ b/macroweave/src/parse.rs @@ -54,7 +54,12 @@ impl Parse for Invocation { pub struct Table { pub names: Vec, - pub rows: Vec>, + pub rows: Vec, +} + +pub struct Row { + pub attrs: TokenStream, + pub values: Vec, } enum Pattern { @@ -175,16 +180,10 @@ fn check_duplicate_names(slots: &[Slot]) -> Result<()> { fn parse_rows(input: ParseStream<'_>, pattern: &Pattern, span: proc_macro2::Span) -> Result { let tokens = input.parse::()?; - let rows = match pattern { - Pattern::Single(_) => split_values(tokens)? - .into_iter() - .map(|value| pattern.bind(vec![value])) - .collect::>>()?, - Pattern::Tuple(_) => split_values(tokens)? - .into_iter() - .map(|row| parse_tuple_row(row).and_then(|values| pattern.bind(values))) - .collect::>>()?, - }; + let rows = split_values(tokens)? + .into_iter() + .map(|row| parse_row(row, pattern)) + .collect::>>()?; if rows.is_empty() { return Err(Error::new(span, "input list must contain at least one row")); @@ -196,6 +195,62 @@ fn parse_rows(input: ParseStream<'_>, pattern: &Pattern, span: proc_macro2::Span }) } +fn parse_row(tokens: TokenStream, pattern: &Pattern) -> Result { + let (attrs, tokens) = split_outer_attrs(tokens); + if is_empty(&tokens) { + return Err(Error::new_spanned( + attrs, + "expected row value after attribute", + )); + } + + let values = match pattern { + Pattern::Single(_) => pattern.bind(vec![tokens])?, + Pattern::Tuple(_) => parse_tuple_row(tokens).and_then(|values| pattern.bind(values))?, + }; + + Ok(Row { attrs, values }) +} + +fn split_outer_attrs(tokens: TokenStream) -> (TokenStream, TokenStream) { + let mut iter = tokens.into_iter().peekable(); + let mut attrs = TokenStream::new(); + + while let Some(TokenTree::Punct(hash)) = iter.peek() { + if hash.as_char() != '#' { + break; + } + + let hash = match iter.next() { + Some(TokenTree::Punct(hash)) => hash, + _ => unreachable!(), + }; + + let Some(TokenTree::Group(group)) = iter.peek() else { + let mut rest = TokenStream::new(); + rest.extend([TokenTree::Punct(hash)]); + rest.extend(iter); + return (attrs, rest); + }; + + if group.delimiter() != Delimiter::Bracket { + let mut rest = TokenStream::new(); + rest.extend([TokenTree::Punct(hash)]); + rest.extend(iter); + return (attrs, rest); + } + + let group = match iter.next() { + Some(TokenTree::Group(group)) => group, + _ => unreachable!(), + }; + + attrs.extend([TokenTree::Punct(hash), TokenTree::Group(group)]); + } + + (attrs, iter.collect()) +} + fn parse_tuple_row(tokens: TokenStream) -> Result> { let mut iter = tokens.clone().into_iter(); let Some(TokenTree::Group(group)) = iter.next() else { diff --git a/macroweave/tests/tests.rs b/macroweave/tests/tests.rs index 4eddcb1..14cd267 100644 --- a/macroweave/tests/tests.rs +++ b/macroweave/tests/tests.rs @@ -163,6 +163,36 @@ fn repeat_ignores_underscore_bindings() { assert_eq!(names, ["TypeA", "TypeB"]); } +#[test] +fn repeat_applies_row_attributes_to_items() { + repeat!((Name, Value) in [ + (ROW_ATTR_INCLUDED, 7usize), + #[cfg(any())] + (ROW_ATTR_DISABLED, compile_error!("disabled repeat row was emitted")), + ] { + const Name: usize = Value; + }); + + assert_eq!(ROW_ATTR_INCLUDED, 7); +} + +#[test] +fn repeat_applies_row_attributes_to_statements() { + let mut values = vec![]; + + repeat!(value in [ + #[cfg(all())] + 1usize, + #[cfg(any())] + compile_error!("disabled statement row was emitted"), + 2usize, + ] { + values.push(value); + }); + + assert_eq!(values, [1, 2]); +} + #[test] fn repeat_expands_nested_repeats() { let mut names = vec![]; @@ -236,6 +266,27 @@ fn splice_expands_match_arms() { assert_eq!(describe(MyType::Other(3)), ""); } +#[test] +fn splice_applies_row_attributes_to_match_arms() { + fn classify(value: u8) -> usize { + splice!((Pat, Output) in [ + (0u8, 10usize), + #[cfg(any())] + (1u8, compile_error!("disabled splice row was emitted")), + (2u8, 20usize), + ] { + match value { + #( Pat => Output ),*, + _ => 99, + } + }) + } + + assert_eq!(classify(0), 10); + assert_eq!(classify(1), 99); + assert_eq!(classify(2), 20); +} + splice!(Variant in [First, Second] { #[derive(Debug, PartialEq, Eq)] enum SpliceEnum {