@@ -141,7 +141,12 @@ fn parse_query_fields(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
141141 // Parse the field's pattern. To support repetition like
142142 // `field: (kind)* @cap`, parse the atom first, then check for
143143 // a quantifier, and lastly handle a trailing `@capture`.
144- let atom = parse_query_atom ( tokens) ?;
144+ // `field: @cap` is sugar for `field: _ @cap`.
145+ let atom = if peek_is_at ( tokens) {
146+ quote ! { yeast:: query:: QueryNode :: Any { match_unnamed: true } }
147+ } else {
148+ parse_query_atom ( tokens) ?
149+ } ;
145150 if peek_is_repetition ( tokens) {
146151 let rep = expect_repetition ( tokens) ?;
147152 let elem = quote ! {
@@ -259,6 +264,7 @@ fn parse_query_list(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
259264 yeast:: query:: QueryListElem :: SingleNode ( #node)
260265 } ,
261266 ) ?;
267+ let elem = maybe_wrap_list_capture ( tokens, elem) ?;
262268 elems. push ( elem) ;
263269 continue ;
264270 }
@@ -276,6 +282,7 @@ fn parse_query_list(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
276282 yeast:: query:: QueryListElem :: SingleNode ( #node)
277283 } ,
278284 ) ?;
285+ let elem = maybe_wrap_list_capture ( tokens, elem) ?;
279286 elems. push ( elem) ;
280287 continue ;
281288 }
@@ -389,8 +396,10 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
389396 let expr = group. stream ( ) ;
390397 return Ok ( quote ! {
391398 {
392- let __value = yeast:: YeastDisplay :: yeast_to_string( & ( #expr) , & * #ctx. ast) ;
393- #ctx. literal( #kind_str, & __value)
399+ let __expr = ( #expr) ;
400+ let __value = yeast:: YeastDisplay :: yeast_to_string( & __expr, & * #ctx. ast) ;
401+ let __source_range = yeast:: YeastSourceRange :: yeast_source_range( & __expr, & * #ctx. ast) ;
402+ #ctx. literal_with_source_range( #kind_str, & __value, __source_range)
394403 }
395404 } ) ;
396405 }
@@ -419,23 +428,35 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
419428 ) ;
420429 field_counter += 1 ;
421430
422- // Check for field: {..expr} — splice a Vec<Id> into the field
431+ // Check for field: {..expr}.chain or field: {expr}.chain — splice a Vec<Id> into the field
423432 if peek_is_group ( tokens, Delimiter :: Brace ) {
424433 let group_clone = tokens. clone ( ) . next ( ) . unwrap ( ) ;
425434 if let TokenTree :: Group ( g) = & group_clone {
426435 let mut inner_check = g. stream ( ) . into_iter ( ) ;
427436 let is_splice = matches ! ( inner_check. next( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' )
428437 && matches ! ( inner_check. next( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
429- if is_splice {
438+ // Determine if a chain (.map(..)) follows the `{}` group.
439+ let mut after = tokens. clone ( ) ;
440+ after. next ( ) ; // skip the brace group
441+ let has_chain = matches ! ( after. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
442+
443+ if is_splice || has_chain {
430444 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 ( ) ;
445+ let base: TokenStream = if is_splice {
446+ let mut inner = group. stream ( ) . into_iter ( ) . peekable ( ) ;
447+ inner. next ( ) ; // consume first .
448+ inner. next ( ) ; // consume second .
449+ let expr: TokenStream = inner. collect ( ) ;
450+ quote ! {
451+ ( #expr) . into_iter( ) . map( :: std:: convert:: Into :: <usize >:: into)
452+ }
453+ } else {
454+ let expr = group. stream ( ) ;
455+ quote ! { ( #expr) . into_iter( ) }
456+ } ;
457+ let chained = parse_chain_suffix ( tokens, ctx, base) ?;
435458 stmts. push ( quote ! {
436- let #temp: Vec <usize > = ( #expr) . into_iter( )
437- . map( :: std:: convert:: Into :: <usize >:: into)
438- . collect( ) ;
459+ let #temp: Vec <usize > = #chained. collect( ) ;
439460 } ) ;
440461 // An empty splice means the field is absent — skip it
441462 // entirely rather than emitting an empty named field.
@@ -472,6 +493,98 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
472493 } )
473494}
474495
496+ /// Parse a chain of `.method(args)` suffixes after a `{expr}` or `{..expr}`
497+ /// placeholder in tree templates. Currently supports:
498+ ///
499+ /// ```text
500+ /// .map(param -> template) -- iterator map: produces Vec<usize>
501+ /// ```
502+ ///
503+ /// The chain may be empty (returns `base` unchanged). Multiple chained calls
504+ /// are supported, e.g. `.map(p -> ...).map(q -> ...)`.
505+ ///
506+ /// Each call expects the receiver to be an iterator. The `base` argument
507+ /// should therefore already be an iterator (use `.into_iter()` on it before
508+ /// calling this function).
509+ fn parse_chain_suffix (
510+ tokens : & mut Tokens ,
511+ ctx : & Ident ,
512+ base : TokenStream ,
513+ ) -> Result < TokenStream > {
514+ let mut current = base;
515+ while matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) {
516+ tokens. next ( ) ; // consume .
517+ let method = expect_ident ( tokens, "expected method name after `.`" ) ?;
518+ let method_str = method. to_string ( ) ;
519+ let args_group = expect_group ( tokens, Delimiter :: Parenthesis ) ?;
520+ match method_str. as_str ( ) {
521+ "map" => {
522+ let mut inner = args_group. stream ( ) . into_iter ( ) . peekable ( ) ;
523+ let param = expect_ident ( & mut inner, "expected lambda parameter name" ) ?;
524+ expect_punct ( & mut inner, '-' , "expected `->` after lambda parameter" ) ?;
525+ expect_punct ( & mut inner, '>' , "expected `->` after lambda parameter" ) ?;
526+ let body = parse_direct_node ( & mut inner, ctx) ?;
527+ if let Some ( tok) = inner. next ( ) {
528+ return Err ( syn:: Error :: new_spanned (
529+ tok,
530+ "unexpected token after lambda body" ,
531+ ) ) ;
532+ }
533+ current = quote ! {
534+ #current. map( |#param| #body)
535+ } ;
536+ }
537+ "reduce_left" => {
538+ // Syntax: reduce_left(first -> init_tpl, acc, elem -> fold_tpl)
539+ // - first -> init_tpl : converts the first element to the initial accumulator
540+ // - acc, elem -> fold_tpl : fold step (acc = current accumulator, elem = next element)
541+ // Empty iterator produces an empty iterator; non-empty produces a single-element iterator.
542+ let mut inner = args_group. stream ( ) . into_iter ( ) . peekable ( ) ;
543+ let init_param = expect_ident ( & mut inner, "expected initial lambda parameter" ) ?;
544+ expect_punct ( & mut inner, '-' , "expected `->` after init parameter" ) ?;
545+ expect_punct ( & mut inner, '>' , "expected `->` after init parameter" ) ?;
546+ let init_body = parse_direct_node ( & mut inner, ctx) ?;
547+ expect_punct ( & mut inner, ',' , "expected `,` after init template" ) ?;
548+ let acc_param = expect_ident ( & mut inner, "expected accumulator parameter" ) ?;
549+ expect_punct ( & mut inner, ',' , "expected `,` after accumulator parameter" ) ?;
550+ let elem_param = expect_ident ( & mut inner, "expected element parameter" ) ?;
551+ expect_punct ( & mut inner, '-' , "expected `->` after element parameter" ) ?;
552+ expect_punct ( & mut inner, '>' , "expected `->` after element parameter" ) ?;
553+ let fold_body = parse_direct_node ( & mut inner, ctx) ?;
554+ if let Some ( tok) = inner. next ( ) {
555+ return Err ( syn:: Error :: new_spanned (
556+ tok,
557+ "unexpected token after fold template" ,
558+ ) ) ;
559+ }
560+ current = quote ! {
561+ {
562+ let mut __iter = #current;
563+ let __result: Option <usize > = if let Some ( #init_param) = __iter. next( ) {
564+ let mut __acc: usize = #init_body;
565+ for #elem_param in __iter {
566+ let #acc_param: usize = __acc;
567+ __acc = #fold_body;
568+ }
569+ Some ( __acc)
570+ } else {
571+ None
572+ } ;
573+ __result. into_iter( )
574+ }
575+ } ;
576+ }
577+ _ => {
578+ return Err ( syn:: Error :: new_spanned (
579+ method,
580+ format ! ( "unknown builtin method `.{method_str}()`" ) ,
581+ ) ) ;
582+ }
583+ }
584+ }
585+ Ok ( current)
586+ }
587+
475588/// Parse the top-level list of a `trees!` template.
476589/// Each item is a node template or `{expr}` splice.
477590fn parse_direct_list ( tokens : & mut Tokens , ctx : & Ident ) -> Result < Vec < TokenStream > > {
@@ -492,18 +605,27 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream
492605 continue ;
493606 }
494607
495- // {expr} or {..expr} — single node or splice
608+ // {expr} or {..expr} (with optional .chain) — single node or splice
496609 if peek_is_group ( tokens, Delimiter :: Brace ) {
497610 let group = expect_group ( tokens, Delimiter :: Brace ) ?;
611+ let has_chain = matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
498612 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(
613+ let is_splice = peek_is_dotdot ( & inner) ;
614+ if is_splice || has_chain {
615+ let base: TokenStream = if is_splice {
616+ inner. next ( ) ; // consume first .
617+ inner. next ( ) ; // consume second .
618+ let expr: TokenStream = inner. collect ( ) ;
619+ quote ! {
505620 ( #expr) . into_iter( ) . map( :: std:: convert:: Into :: <usize >:: into)
506- ) ;
621+ }
622+ } else {
623+ let expr = group. stream ( ) ;
624+ quote ! { ( #expr) . into_iter( ) }
625+ } ;
626+ let chained = parse_chain_suffix ( tokens, ctx, base) ?;
627+ items. push ( quote ! {
628+ __nodes. extend( #chained) ;
507629 } ) ;
508630 } else {
509631 let expr = group. stream ( ) ;
@@ -604,8 +726,11 @@ fn extract_captures_inner(
604726 }
605727 last_mult = CaptureMultiplicity :: Single ;
606728 }
607- TokenTree :: Punct ( p) if matches ! ( p. as_char( ) , '*' | '+' | '?' ) => {
608- // Keep last_mult — the @capture follows
729+ TokenTree :: Punct ( p) if p. as_char ( ) == '*' || p. as_char ( ) == '+' => {
730+ last_mult = CaptureMultiplicity :: Repeated ;
731+ }
732+ TokenTree :: Punct ( p) if p. as_char ( ) == '?' => {
733+ last_mult = CaptureMultiplicity :: Optional ;
609734 }
610735 _ => {
611736 last_mult = CaptureMultiplicity :: Single ;
0 commit comments