Skip to content

Commit 28c879f

Browse files
asgerfCopilot
andcommitted
yeast-macros: add .map(p -> tpl) chain syntax for tree templates
After a {expr} or {..expr} placeholder, an optional chain of .<builtin>() calls may follow. Currently the only builtin is: .map(param -> template) which applies the template to each element of the iterable and collects the resulting node IDs. A chain auto-splices into the enclosing field/child position. Example: path: {parts}.map(p -> (identifier #{p})) The framework is extensible: additional builtins can be added by matching on the method name in parse_chain_suffix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6000c18 commit 28c879f

2 files changed

Lines changed: 96 additions & 17 deletions

File tree

shared/yeast-macros/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ pub fn query(input: TokenStream) -> TokenStream {
4444
/// {expr} - embed a Rust expression returning Id
4545
/// {..expr} - splice an iterable of Id (in child/field position)
4646
/// field: {..expr} - splice into a named field
47+
/// {expr}.map(p -> tpl) - apply tpl to each element; splice result
4748
/// ```
4849
///
50+
/// Chain syntax after `{expr}` or `{..expr}`:
51+
/// - `.map(param -> template)` — produces one node per element of the iterable.
52+
/// The lambda parameter is bound in `template` (e.g. `{parts}.map(p -> (identifier #{p}))`).
53+
/// - Chains always splice (the result is iterable).
54+
///
4955
/// Can be called with an explicit context or using the implicit context
5056
/// from an enclosing `rule!`:
5157
///

shared/yeast-macros/src/parse.rs

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -419,23 +419,35 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
419419
);
420420
field_counter += 1;
421421

422-
// Check for field: {..expr} — splice a Vec<Id> into the field
422+
// Check for field: {..expr}.chain or field: {expr}.chain — splice a Vec<Id> into the field
423423
if peek_is_group(tokens, Delimiter::Brace) {
424424
let group_clone = tokens.clone().next().unwrap();
425425
if let TokenTree::Group(g) = &group_clone {
426426
let mut inner_check = g.stream().into_iter();
427427
let is_splice = matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.')
428428
&& matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
429-
if is_splice {
429+
// Determine if a chain (.map(..)) follows the `{}` group.
430+
let mut after = tokens.clone();
431+
after.next(); // skip the brace group
432+
let has_chain = matches!(after.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
433+
434+
if is_splice || has_chain {
430435
let group = expect_group(tokens, Delimiter::Brace)?;
431-
let mut inner = group.stream().into_iter().peekable();
432-
inner.next(); // consume first .
433-
inner.next(); // consume second .
434-
let expr: proc_macro2::TokenStream = inner.collect();
436+
let base: TokenStream = if is_splice {
437+
let mut inner = group.stream().into_iter().peekable();
438+
inner.next(); // consume first .
439+
inner.next(); // consume second .
440+
let expr: TokenStream = inner.collect();
441+
quote! {
442+
(#expr).into_iter().map(::std::convert::Into::<usize>::into)
443+
}
444+
} else {
445+
let expr = group.stream();
446+
quote! { (#expr).into_iter() }
447+
};
448+
let chained = parse_chain_suffix(tokens, ctx, base)?;
435449
stmts.push(quote! {
436-
let #temp: Vec<usize> = (#expr).into_iter()
437-
.map(::std::convert::Into::<usize>::into)
438-
.collect();
450+
let #temp: Vec<usize> = #chained.collect();
439451
});
440452
// An empty splice means the field is absent — skip it
441453
// entirely rather than emitting an empty named field.
@@ -472,6 +484,58 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
472484
})
473485
}
474486

487+
/// Parse a chain of `.method(args)` suffixes after a `{expr}` or `{..expr}`
488+
/// placeholder in tree templates. Currently supports:
489+
///
490+
/// ```text
491+
/// .map(param -> template) -- iterator map: produces Vec<usize>
492+
/// ```
493+
///
494+
/// The chain may be empty (returns `base` unchanged). Multiple chained calls
495+
/// are supported, e.g. `.map(p -> ...).map(q -> ...)`.
496+
///
497+
/// Each call expects the receiver to be an iterator. The `base` argument
498+
/// should therefore already be an iterator (use `.into_iter()` on it before
499+
/// calling this function).
500+
fn parse_chain_suffix(
501+
tokens: &mut Tokens,
502+
ctx: &Ident,
503+
base: TokenStream,
504+
) -> Result<TokenStream> {
505+
let mut current = base;
506+
while matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.') {
507+
tokens.next(); // consume .
508+
let method = expect_ident(tokens, "expected method name after `.`")?;
509+
let method_str = method.to_string();
510+
let args_group = expect_group(tokens, Delimiter::Parenthesis)?;
511+
match method_str.as_str() {
512+
"map" => {
513+
let mut inner = args_group.stream().into_iter().peekable();
514+
let param = expect_ident(&mut inner, "expected lambda parameter name")?;
515+
expect_punct(&mut inner, '-', "expected `->` after lambda parameter")?;
516+
expect_punct(&mut inner, '>', "expected `->` after lambda parameter")?;
517+
let body = parse_direct_node(&mut inner, ctx)?;
518+
if let Some(tok) = inner.next() {
519+
return Err(syn::Error::new_spanned(
520+
tok,
521+
"unexpected token after lambda body",
522+
));
523+
}
524+
current = quote! {
525+
#current.map(|#param| #body)
526+
};
527+
}
528+
_ => {
529+
return Err(syn::Error::new_spanned(
530+
method,
531+
format!("unknown builtin method `.{method_str}()`"),
532+
));
533+
}
534+
}
535+
}
536+
Ok(current)
537+
}
538+
475539
/// Parse the top-level list of a `trees!` template.
476540
/// Each item is a node template or `{expr}` splice.
477541
fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream>> {
@@ -492,18 +556,27 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream
492556
continue;
493557
}
494558

495-
// {expr} or {..expr} — single node or splice
559+
// {expr} or {..expr} (with optional .chain) — single node or splice
496560
if peek_is_group(tokens, Delimiter::Brace) {
497561
let group = expect_group(tokens, Delimiter::Brace)?;
562+
let has_chain = matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
498563
let mut inner = group.stream().into_iter().peekable();
499-
if peek_is_dotdot(&inner) {
500-
inner.next(); // consume first .
501-
inner.next(); // consume second .
502-
let expr: TokenStream = inner.collect();
503-
items.push(quote! {
504-
__nodes.extend(
564+
let is_splice = peek_is_dotdot(&inner);
565+
if is_splice || has_chain {
566+
let base: TokenStream = if is_splice {
567+
inner.next(); // consume first .
568+
inner.next(); // consume second .
569+
let expr: TokenStream = inner.collect();
570+
quote! {
505571
(#expr).into_iter().map(::std::convert::Into::<usize>::into)
506-
);
572+
}
573+
} else {
574+
let expr = group.stream();
575+
quote! { (#expr).into_iter() }
576+
};
577+
let chained = parse_chain_suffix(tokens, ctx, base)?;
578+
items.push(quote! {
579+
__nodes.extend(#chained);
507580
});
508581
} else {
509582
let expr = group.stream();

0 commit comments

Comments
 (0)