Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- macroweave-docs-end -->
Expand Down
6 changes: 4 additions & 2 deletions macroweave/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ pub fn repeat(input: TokenStream) -> Result<TokenStream> {

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(),
));
}
Expand Down Expand Up @@ -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(),
));
}
Expand Down
77 changes: 66 additions & 11 deletions macroweave/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ impl Parse for Invocation {

pub struct Table {
pub names: Vec<Ident>,
pub rows: Vec<Vec<TokenStream>>,
pub rows: Vec<Row>,
}

pub struct Row {
pub attrs: TokenStream,
pub values: Vec<TokenStream>,
}

enum Pattern {
Expand Down Expand Up @@ -175,16 +180,10 @@ fn check_duplicate_names(slots: &[Slot]) -> Result<()> {

fn parse_rows(input: ParseStream<'_>, pattern: &Pattern, span: proc_macro2::Span) -> Result<Table> {
let tokens = input.parse::<TokenStream>()?;
let rows = match pattern {
Pattern::Single(_) => split_values(tokens)?
.into_iter()
.map(|value| pattern.bind(vec![value]))
.collect::<Result<Vec<_>>>()?,
Pattern::Tuple(_) => split_values(tokens)?
.into_iter()
.map(|row| parse_tuple_row(row).and_then(|values| pattern.bind(values)))
.collect::<Result<Vec<_>>>()?,
};
let rows = split_values(tokens)?
.into_iter()
.map(|row| parse_row(row, pattern))
.collect::<Result<Vec<_>>>()?;

if rows.is_empty() {
return Err(Error::new(span, "input list must contain at least one row"));
Expand All @@ -196,6 +195,62 @@ fn parse_rows(input: ParseStream<'_>, pattern: &Pattern, span: proc_macro2::Span
})
}

fn parse_row(tokens: TokenStream, pattern: &Pattern) -> Result<Row> {
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<Vec<TokenStream>> {
let mut iter = tokens.clone().into_iter();
let Some(TokenTree::Group(group)) = iter.next() else {
Expand Down
51 changes: 51 additions & 0 deletions macroweave/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];
Expand Down Expand Up @@ -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 {
Expand Down