Skip to content

Commit 0006894

Browse files
asgerfCopilot
andcommitted
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>
1 parent 28c879f commit 0006894

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

shared/yeast-macros/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ pub fn query(input: TokenStream) -> TokenStream {
4545
/// {..expr} - splice an iterable of Id (in child/field position)
4646
/// field: {..expr} - splice into a named field
4747
/// {expr}.map(p -> tpl) - apply tpl to each element; splice result
48+
/// {expr}.reduce_left(f -> init, acc, e -> fold)
49+
/// - fold with per-element init; splice 0 or 1 result
4850
/// ```
4951
///
5052
/// 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+
/// - `.map(param -> template)` — one output node per input element.
54+
/// - `.reduce_left(first -> init, acc, elem -> fold)` — fold left; the first
55+
/// element is converted by `init`, subsequent elements are folded by `fold`
56+
/// with the accumulator bound to `acc`. An empty iterable yields nothing.
5357
/// - Chains always splice (the result is iterable).
58+
/// - Multiple chains can be chained, e.g. `.map(...).reduce_left(...)`.
5459
///
5560
/// Can be called with an explicit context or using the implicit context
5661
/// from an enclosing `rule!`:

shared/yeast-macros/src/parse.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,46 @@ fn parse_chain_suffix(
525525
#current.map(|#param| #body)
526526
};
527527
}
528+
"reduce_left" => {
529+
// Syntax: reduce_left(first -> init_tpl, acc, elem -> fold_tpl)
530+
// - first -> init_tpl : converts the first element to the initial accumulator
531+
// - acc, elem -> fold_tpl : fold step (acc = current accumulator, elem = next element)
532+
// Empty iterator produces an empty iterator; non-empty produces a single-element iterator.
533+
let mut inner = args_group.stream().into_iter().peekable();
534+
let init_param = expect_ident(&mut inner, "expected initial lambda parameter")?;
535+
expect_punct(&mut inner, '-', "expected `->` after init parameter")?;
536+
expect_punct(&mut inner, '>', "expected `->` after init parameter")?;
537+
let init_body = parse_direct_node(&mut inner, ctx)?;
538+
expect_punct(&mut inner, ',', "expected `,` after init template")?;
539+
let acc_param = expect_ident(&mut inner, "expected accumulator parameter")?;
540+
expect_punct(&mut inner, ',', "expected `,` after accumulator parameter")?;
541+
let elem_param = expect_ident(&mut inner, "expected element parameter")?;
542+
expect_punct(&mut inner, '-', "expected `->` after element parameter")?;
543+
expect_punct(&mut inner, '>', "expected `->` after element parameter")?;
544+
let fold_body = parse_direct_node(&mut inner, ctx)?;
545+
if let Some(tok) = inner.next() {
546+
return Err(syn::Error::new_spanned(
547+
tok,
548+
"unexpected token after fold template",
549+
));
550+
}
551+
current = quote! {
552+
{
553+
let mut __iter = #current;
554+
let __result: Option<usize> = if let Some(#init_param) = __iter.next() {
555+
let mut __acc: usize = #init_body;
556+
for #elem_param in __iter {
557+
let #acc_param: usize = __acc;
558+
__acc = #fold_body;
559+
}
560+
Some(__acc)
561+
} else {
562+
None
563+
};
564+
__result.into_iter()
565+
}
566+
};
567+
}
528568
_ => {
529569
return Err(syn::Error::new_spanned(
530570
method,

0 commit comments

Comments
 (0)