@@ -645,6 +645,32 @@ enum CaptureMultiplicity {
645645 Repeated ,
646646}
647647
648+ fn capture_bindings < ' a > ( captures : impl Iterator < Item = & ' a CaptureInfo > ) -> Vec < TokenStream > {
649+ captures
650+ . map ( |cap| {
651+ let name = Ident :: new ( & cap. name , Span :: call_site ( ) ) ;
652+ let name_str = & cap. name ;
653+ match cap. multiplicity {
654+ CaptureMultiplicity :: Repeated => {
655+ quote ! {
656+ let #name: Vec <yeast:: Id > = __captures. get_all( #name_str) ;
657+ }
658+ }
659+ CaptureMultiplicity :: Optional => {
660+ quote ! {
661+ let #name: Option <yeast:: Id > = __captures. get_opt( #name_str) ;
662+ }
663+ }
664+ CaptureMultiplicity :: Single => {
665+ quote ! {
666+ let #name: yeast:: Id = __captures. get_var( #name_str) . unwrap( ) ;
667+ }
668+ }
669+ }
670+ } )
671+ . collect ( )
672+ }
673+
648674/// Walk a token stream and extract all `@name` captures, noting whether
649675/// they appear after `*` or `+` (repeated) or not.
650676fn extract_captures ( stream : & TokenStream ) -> Vec < CaptureInfo > {
@@ -806,7 +832,7 @@ fn try_consume_return_annotation(tokens: &mut Tokens) -> Result<Option<ReturnAnn
806832pub fn parse_rule_top ( input : TokenStream ) -> Result < TokenStream > {
807833 let mut tokens = input. into_iter ( ) . peekable ( ) ;
808834
809- // Collect query tokens up to `=>`
835+ // Collect query and optional `where` guard tokens up to `=>`.
810836 let mut query_tokens = Vec :: new ( ) ;
811837 loop {
812838 match tokens. peek ( ) {
@@ -830,48 +856,30 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
830856 }
831857 }
832858
833- let query_stream: TokenStream = query_tokens . into_iter ( ) . collect ( ) ;
859+ let ( query_stream, guard ) = split_rule_guard ( query_tokens ) ? ;
834860
835861 // Extract captures from query
836862 let captures = extract_captures ( & query_stream) ;
837863
838864 // Parse query
839865 let query_code = parse_query_top ( query_stream. clone ( ) ) ?;
840866
867+ let ( raw_captures, translated_captures) : ( Vec < _ > , Vec < _ > ) =
868+ captures. iter ( ) . partition ( |capture| capture. raw ) ;
869+
841870 // Capture names marked `@@name` (raw) — passed to the auto-translate
842871 // prefix as a skip list so those captures keep their input-schema ids.
843- let raw_capture_names: Vec < & str > = captures
872+ let raw_capture_names: Vec < & str > = raw_captures
844873 . iter ( )
845- . filter ( |c| c. raw )
846- . map ( |c| c. name . as_str ( ) )
874+ . map ( |capture| capture. name . as_str ( ) )
847875 . collect ( ) ;
848876
849- // Generate capture bindings
877+ // Both capture sets are bound raw in the guard, which runs before
878+ // auto-translation. In the transform, raw captures remain unchanged
879+ // while translated captures are bound after auto-translation.
850880 let ctx_ident = Ident :: new ( IMPLICIT_CTX , Span :: call_site ( ) ) ;
851- let bindings: Vec < TokenStream > = captures
852- . iter ( )
853- . map ( |cap| {
854- let name = Ident :: new ( & cap. name , Span :: call_site ( ) ) ;
855- let name_str = & cap. name ;
856- match cap. multiplicity {
857- CaptureMultiplicity :: Repeated => {
858- quote ! {
859- let #name: Vec <yeast:: Id > = __captures. get_all( #name_str) ;
860- }
861- }
862- CaptureMultiplicity :: Optional => {
863- quote ! {
864- let #name: Option <yeast:: Id > = __captures. get_opt( #name_str) ;
865- }
866- }
867- CaptureMultiplicity :: Single => {
868- quote ! {
869- let #name: yeast:: Id = __captures. get_var( #name_str) . unwrap( ) ;
870- }
871- }
872- }
873- } )
874- . collect ( ) ;
881+ let raw_bindings = capture_bindings ( raw_captures. into_iter ( ) ) ;
882+ let translated_bindings = capture_bindings ( translated_captures. into_iter ( ) ) ;
875883
876884 // Parse transform: the token(s) after `=>` fall into one of three
877885 // shapes, dispatched in order:
@@ -1015,24 +1023,36 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
10151023 }
10161024 } ;
10171025
1026+ let guard = guard. unwrap_or_else ( || syn:: parse_quote!( true ) ) ;
10181027 Ok ( quote ! {
10191028 {
10201029 let __query = #query_code;
1021- yeast:: Rule :: new( __query, Box :: new( |__ast: & mut yeast:: Ast , mut __captures: yeast:: captures:: Captures , __fresh: & yeast:: tree_builder:: FreshScope , __source_range: Option <yeast:: Range >, __user_ctx: & mut _, __translator: yeast:: TranslatorHandle <' _, _>| {
1022- // Auto-translation prefix: recursively translate every
1023- // captured node before invoking the user's transform body,
1024- // except for `@@name` captures listed in `__skip` which the
1025- // body consumes raw.
1026- // For OneShot rules this preserves the legacy behaviour
1027- // (input-schema captures translated to output-schema
1028- // nodes); for Repeating rules it is a no-op.
1029- let __skip: & [ & str ] = & [ #( #raw_capture_names) , * ] ;
1030- __translator. auto_translate_captures( & mut __captures, __ast, __user_ctx, __skip) ?;
1031- #( #bindings) *
1032- let mut #ctx_ident = yeast:: build:: BuildCtx :: with_translator( __ast, & __captures, __fresh, __source_range, __user_ctx, __translator) ;
1033- let __result: Vec <yeast:: Id > = { #transform_body } ;
1034- Ok ( __result)
1035- } ) )
1030+ yeast:: Rule :: guarded(
1031+ __query,
1032+ Box :: new( |__ast: & yeast:: Ast , __captures: & yeast:: captures:: Captures , __user_ctx: & mut _| {
1033+ #( #raw_bindings) *
1034+ #( #translated_bindings) *
1035+ let ast = __ast;
1036+ let #ctx_ident = __user_ctx;
1037+ Ok ( #guard)
1038+ } ) ,
1039+ Box :: new( |__ast: & mut yeast:: Ast , mut __captures: yeast:: captures:: Captures , __fresh: & yeast:: tree_builder:: FreshScope , __source_range: Option <yeast:: Range >, __user_ctx: & mut _, __translator: yeast:: TranslatorHandle <' _, _>| {
1040+ // Auto-translation prefix: recursively translate every
1041+ // captured node before invoking the user's transform body,
1042+ // except for `@@name` captures listed in `__skip` which the
1043+ // body consumes raw.
1044+ // For OneShot rules this preserves the legacy behaviour
1045+ // (input-schema captures translated to output-schema
1046+ // nodes); for Repeating rules it is a no-op.
1047+ let __skip: & [ & str ] = & [ #( #raw_capture_names) , * ] ;
1048+ __translator. auto_translate_captures( & mut __captures, __ast, __user_ctx, __skip) ?;
1049+ #( #raw_bindings) *
1050+ #( #translated_bindings) *
1051+ let mut #ctx_ident = yeast:: build:: BuildCtx :: with_translator( __ast, & __captures, __fresh, __source_range, __user_ctx, __translator) ;
1052+ let __result: Vec <yeast:: Id > = { #transform_body } ;
1053+ Ok ( __result)
1054+ } ) ,
1055+ )
10361056 }
10371057 } )
10381058}
@@ -1041,6 +1061,37 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
10411061// Token utilities
10421062// ---------------------------------------------------------------------------
10431063
1064+ /// Split the tokens before a rule's `=>` into its query and optional
1065+ /// top-level `where guard`. Groups are opaque `TokenTree`s, so a `where`
1066+ /// inside the query or guard expression is not mistaken for the separator.
1067+ fn split_rule_guard ( tokens : Vec < TokenTree > ) -> Result < ( TokenStream , Option < syn:: Expr > ) > {
1068+ let guard_index = tokens
1069+ . iter ( )
1070+ . position ( |tok| matches ! ( tok, TokenTree :: Ident ( ident) if ident == "where" ) ) ;
1071+
1072+ let Some ( guard_index) = guard_index else {
1073+ return Ok ( ( tokens. into_iter ( ) . collect ( ) , None ) ) ;
1074+ } ;
1075+
1076+ let query: TokenStream = tokens[ ..guard_index] . iter ( ) . cloned ( ) . collect ( ) ;
1077+ if query. is_empty ( ) {
1078+ return Err ( syn:: Error :: new (
1079+ Span :: call_site ( ) ,
1080+ "expected query before rule guard" ,
1081+ ) ) ;
1082+ }
1083+
1084+ let guard_tokens: TokenStream = tokens[ guard_index + 1 ..] . iter ( ) . cloned ( ) . collect ( ) ;
1085+ if guard_tokens. is_empty ( ) {
1086+ return Err ( syn:: Error :: new_spanned (
1087+ tokens[ guard_index] . clone ( ) ,
1088+ "expected expression after rule guard `where`" ,
1089+ ) ) ;
1090+ }
1091+ let guard = syn:: parse2 :: < syn:: Expr > ( guard_tokens) ?;
1092+ Ok ( ( query, Some ( guard) ) )
1093+ }
1094+
10441095fn peek_is_at ( tokens : & mut Tokens ) -> bool {
10451096 matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '@' )
10461097}
0 commit comments