diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index 1d7236b500a..3945ddd3e27 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -44,8 +44,14 @@ pub fn query(input: TokenStream) -> TokenStream { /// {expr} - embed a Rust expression returning Id /// {..expr} - splice an iterable of Id (in child/field position) /// field: {..expr} - splice into a named field +/// {expr}.map(p -> tpl) - apply tpl to each element; splice result /// ``` /// +/// Chain syntax after `{expr}` or `{..expr}`: +/// - `.map(param -> template)` — produces one node per element of the iterable. +/// The lambda parameter is bound in `template` (e.g. `{parts}.map(p -> (identifier #{p}))`). +/// - Chains always splice (the result is iterable). +/// /// Can be called with an explicit context or using the implicit context /// from an enclosing `rule!`: /// diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index eb3b161b295..a02538ffec9 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -419,23 +419,35 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result into the field + // Check for field: {..expr}.chain or field: {expr}.chain — splice a Vec into the field if peek_is_group(tokens, Delimiter::Brace) { let group_clone = tokens.clone().next().unwrap(); if let TokenTree::Group(g) = &group_clone { let mut inner_check = g.stream().into_iter(); let is_splice = matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.') && matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.'); - if is_splice { + // Determine if a chain (.map(..)) follows the `{}` group. + let mut after = tokens.clone(); + after.next(); // skip the brace group + let has_chain = matches!(after.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.'); + + if is_splice || has_chain { let group = expect_group(tokens, Delimiter::Brace)?; - let mut inner = group.stream().into_iter().peekable(); - inner.next(); // consume first . - inner.next(); // consume second . - let expr: proc_macro2::TokenStream = inner.collect(); + let base: TokenStream = if is_splice { + let mut inner = group.stream().into_iter().peekable(); + inner.next(); // consume first . + inner.next(); // consume second . + let expr: TokenStream = inner.collect(); + quote! { + (#expr).into_iter().map(::std::convert::Into::::into) + } + } else { + let expr = group.stream(); + quote! { (#expr).into_iter() } + }; + let chained = parse_chain_suffix(tokens, ctx, base)?; stmts.push(quote! { - let #temp: Vec = (#expr).into_iter() - .map(::std::convert::Into::::into) - .collect(); + let #temp: Vec = #chained.collect(); }); // An empty splice means the field is absent — skip it // entirely rather than emitting an empty named field. @@ -472,6 +484,58 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result template) -- iterator map: produces Vec +/// ``` +/// +/// The chain may be empty (returns `base` unchanged). Multiple chained calls +/// are supported, e.g. `.map(p -> ...).map(q -> ...)`. +/// +/// Each call expects the receiver to be an iterator. The `base` argument +/// should therefore already be an iterator (use `.into_iter()` on it before +/// calling this function). +fn parse_chain_suffix( + tokens: &mut Tokens, + ctx: &Ident, + base: TokenStream, +) -> Result { + let mut current = base; + while matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.') { + tokens.next(); // consume . + let method = expect_ident(tokens, "expected method name after `.`")?; + let method_str = method.to_string(); + let args_group = expect_group(tokens, Delimiter::Parenthesis)?; + match method_str.as_str() { + "map" => { + let mut inner = args_group.stream().into_iter().peekable(); + let param = expect_ident(&mut inner, "expected lambda parameter name")?; + expect_punct(&mut inner, '-', "expected `->` after lambda parameter")?; + expect_punct(&mut inner, '>', "expected `->` after lambda parameter")?; + let body = parse_direct_node(&mut inner, ctx)?; + if let Some(tok) = inner.next() { + return Err(syn::Error::new_spanned( + tok, + "unexpected token after lambda body", + )); + } + current = quote! { + #current.map(|#param| #body) + }; + } + _ => { + return Err(syn::Error::new_spanned( + method, + format!("unknown builtin method `.{method_str}()`"), + )); + } + } + } + Ok(current) +} + /// Parse the top-level list of a `trees!` template. /// Each item is a node template or `{expr}` splice. fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result> { @@ -492,18 +556,27 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result::into) - ); + } + } else { + let expr = group.stream(); + quote! { (#expr).into_iter() } + }; + let chained = parse_chain_suffix(tokens, ctx, base)?; + items.push(quote! { + __nodes.extend(#chained); }); } else { let expr = group.stream();