From 00068948c16c229645a8812dace2a0f76f3f3bf7 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 30 May 2026 07:27:51 +0200 Subject: [PATCH] yeast-macros: add .reduce_left(first -> init, acc, elem -> fold) chain A left fold over an iterable where the first element seeds the accumulator: - first -> init : converts the first element to the initial accumulator - acc, elem -> fold : fold step; acc = current accumulator, elem = next element - Empty iterable produces nothing (0-element splice) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast-macros/src/lib.rs | 9 +++++-- shared/yeast-macros/src/parse.rs | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index 3945ddd3e27..07077be51f0 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -45,12 +45,17 @@ pub fn query(input: TokenStream) -> TokenStream { /// {..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 +/// {expr}.reduce_left(f -> init, acc, e -> fold) +/// - fold with per-element init; splice 0 or 1 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}))`). +/// - `.map(param -> template)` — one output node per input element. +/// - `.reduce_left(first -> init, acc, elem -> fold)` — fold left; the first +/// element is converted by `init`, subsequent elements are folded by `fold` +/// with the accumulator bound to `acc`. An empty iterable yields nothing. /// - Chains always splice (the result is iterable). +/// - Multiple chains can be chained, e.g. `.map(...).reduce_left(...)`. /// /// 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 a02538ffec9..5ee3156bcd4 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -525,6 +525,46 @@ fn parse_chain_suffix( #current.map(|#param| #body) }; } + "reduce_left" => { + // Syntax: reduce_left(first -> init_tpl, acc, elem -> fold_tpl) + // - first -> init_tpl : converts the first element to the initial accumulator + // - acc, elem -> fold_tpl : fold step (acc = current accumulator, elem = next element) + // Empty iterator produces an empty iterator; non-empty produces a single-element iterator. + let mut inner = args_group.stream().into_iter().peekable(); + let init_param = expect_ident(&mut inner, "expected initial lambda parameter")?; + expect_punct(&mut inner, '-', "expected `->` after init parameter")?; + expect_punct(&mut inner, '>', "expected `->` after init parameter")?; + let init_body = parse_direct_node(&mut inner, ctx)?; + expect_punct(&mut inner, ',', "expected `,` after init template")?; + let acc_param = expect_ident(&mut inner, "expected accumulator parameter")?; + expect_punct(&mut inner, ',', "expected `,` after accumulator parameter")?; + let elem_param = expect_ident(&mut inner, "expected element parameter")?; + expect_punct(&mut inner, '-', "expected `->` after element parameter")?; + expect_punct(&mut inner, '>', "expected `->` after element parameter")?; + let fold_body = parse_direct_node(&mut inner, ctx)?; + if let Some(tok) = inner.next() { + return Err(syn::Error::new_spanned( + tok, + "unexpected token after fold template", + )); + } + current = quote! { + { + let mut __iter = #current; + let __result: Option = if let Some(#init_param) = __iter.next() { + let mut __acc: usize = #init_body; + for #elem_param in __iter { + let #acc_param: usize = __acc; + __acc = #fold_body; + } + Some(__acc) + } else { + None + }; + __result.into_iter() + } + }; + } _ => { return Err(syn::Error::new_spanned( method,