@@ -22,10 +22,9 @@ pub fn parse_query_top(input: TokenStream) -> Result<TokenStream> {
2222/// Parse a single query node (possibly with a trailing `@capture`).
2323fn parse_query_node ( tokens : & mut Tokens ) -> Result < TokenStream > {
2424 let base = parse_query_atom ( tokens) ?;
25- // Check for trailing @capture
25+ // Check for trailing @capture or @@capture
2626 if peek_is_at ( tokens) {
27- tokens. next ( ) ; // consume @
28- let capture_name = expect_ident ( tokens, "expected capture name after @" ) ?;
27+ let capture_name = consume_capture_marker ( tokens) ?;
2928 let name_str = capture_name. to_string ( ) ;
3029 Ok ( quote ! {
3130 yeast:: query:: QueryNode :: Capture {
@@ -159,8 +158,7 @@ fn parse_query_fields(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
159158 push_field_elem ( & mut field_order, & mut field_elems, field_str, elem) ;
160159 } else {
161160 let child = if peek_is_at ( tokens) {
162- tokens. next ( ) ;
163- let capture_name = expect_ident ( tokens, "expected capture name after @" ) ?;
161+ let capture_name = consume_capture_marker ( tokens) ?;
164162 let name_str = capture_name. to_string ( ) ;
165163 quote ! {
166164 yeast:: query:: QueryNode :: Capture {
@@ -650,6 +648,9 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream
650648struct CaptureInfo {
651649 name : String ,
652650 multiplicity : CaptureMultiplicity ,
651+ /// `true` for `@@name` captures: the auto-translate prefix skips them,
652+ /// so the bound `NodeRef` refers to the raw (input-schema) node.
653+ raw : bool ,
653654}
654655
655656#[ derive( Clone , Copy , PartialEq ) ]
@@ -708,6 +709,14 @@ fn extract_captures_inner(
708709 extract_captures_inner ( & mut inner, captures, child_mult) ;
709710 }
710711 TokenTree :: Punct ( p) if p. as_char ( ) == '@' => {
712+ // `@@name` marks the capture as raw (skip auto-translate).
713+ let raw = matches ! (
714+ tokens. peek( ) ,
715+ Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '@'
716+ ) ;
717+ if raw {
718+ tokens. next ( ) ; // consume the second `@`
719+ }
711720 if let Some ( TokenTree :: Ident ( name) ) = tokens. next ( ) {
712721 let mult = if parent_mult == CaptureMultiplicity :: Repeated
713722 || last_mult == CaptureMultiplicity :: Repeated
@@ -723,6 +732,7 @@ fn extract_captures_inner(
723732 captures. push ( CaptureInfo {
724733 name : name. to_string ( ) ,
725734 multiplicity : mult,
735+ raw,
726736 } ) ;
727737 }
728738 last_mult = CaptureMultiplicity :: Single ;
@@ -776,6 +786,14 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
776786 // Parse query
777787 let query_code = parse_query_top ( query_stream. clone ( ) ) ?;
778788
789+ // Capture names marked `@@name` (raw) — passed to the auto-translate
790+ // prefix as a skip list so those captures keep their input-schema ids.
791+ let raw_capture_names: Vec < & str > = captures
792+ . iter ( )
793+ . filter ( |c| c. raw )
794+ . map ( |c| c. name . as_str ( ) )
795+ . collect ( ) ;
796+
779797 // Generate capture bindings
780798 let ctx_ident = Ident :: new ( IMPLICIT_CTX , Span :: call_site ( ) ) ;
781799 let bindings: Vec < TokenStream > = captures
@@ -891,11 +909,14 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
891909 let __query = #query_code;
892910 yeast:: Rule :: new( __query, Box :: new( |__ast: & mut yeast:: Ast , mut __captures: yeast:: captures:: Captures , __fresh: & yeast:: tree_builder:: FreshScope , __source_range: Option <tree_sitter:: Range >, __user_ctx: & mut _, __translator: yeast:: TranslatorHandle <' _, _>| {
893911 // Auto-translation prefix: recursively translate every
894- // captured node before invoking the user's transform body.
912+ // captured node before invoking the user's transform body,
913+ // except for `@@name` captures listed in `__skip` which the
914+ // body consumes raw.
895915 // For OneShot rules this preserves the legacy behaviour
896916 // (input-schema captures translated to output-schema
897917 // nodes); for Repeating rules it is a no-op.
898- __translator. auto_translate_captures( & mut __captures, __ast, __user_ctx) ?;
918+ let __skip: & [ & str ] = & [ #( #raw_capture_names) , * ] ;
919+ __translator. auto_translate_captures( & mut __captures, __ast, __user_ctx, __skip) ?;
899920 #( #bindings) *
900921 let mut #ctx_ident = yeast:: build:: BuildCtx :: with_translator( __ast, & __captures, __fresh, __source_range, __user_ctx, __translator) ;
901922 let __result: Vec <usize > = { #transform_body } ;
@@ -905,106 +926,6 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
905926 } )
906927}
907928
908- /// Parse `manual_rule!( query { body } )`.
909- ///
910- /// Like [`parse_rule_top`] but:
911- /// - Expects a Rust block `{ ... }` after the query (no `=>` arrow).
912- /// - Generates code that does NOT auto-translate captures before
913- /// running the body. Capture variables refer to raw (input-schema)
914- /// nodes; the body is responsible for explicit translation via
915- /// `ctx.translate(...)`.
916- /// - The body is included verbatim and must evaluate to
917- /// `Result<Vec<usize>, String>`.
918- pub fn parse_manual_rule_top ( input : TokenStream ) -> Result < TokenStream > {
919- let mut tokens = input. into_iter ( ) . peekable ( ) ;
920-
921- // Collect query tokens up to the body block `{ ... }`.
922- let mut query_tokens = Vec :: new ( ) ;
923- loop {
924- match tokens. peek ( ) {
925- None => {
926- return Err ( syn:: Error :: new (
927- Span :: call_site ( ) ,
928- "expected a Rust block `{ ... }` after the query in manual_rule!" ,
929- ) )
930- }
931- Some ( TokenTree :: Group ( g) ) if g. delimiter ( ) == Delimiter :: Brace => break ,
932- _ => {
933- query_tokens. push ( tokens. next ( ) . unwrap ( ) ) ;
934- }
935- }
936- }
937-
938- let query_stream: TokenStream = query_tokens. into_iter ( ) . collect ( ) ;
939-
940- // Extract captures from the query (same as in `rule!`).
941- let captures = extract_captures ( & query_stream) ;
942-
943- // Parse the query into the QueryNode-building expression.
944- let query_code = parse_query_top ( query_stream) ?;
945-
946- // Generate capture bindings (same as in `rule!`).
947- let ctx_ident = Ident :: new ( IMPLICIT_CTX , Span :: call_site ( ) ) ;
948- let bindings: Vec < TokenStream > = captures
949- . iter ( )
950- . map ( |cap| {
951- let name = Ident :: new ( & cap. name , Span :: call_site ( ) ) ;
952- let name_str = & cap. name ;
953- match cap. multiplicity {
954- CaptureMultiplicity :: Repeated => quote ! {
955- let #name: Vec <yeast:: NodeRef > = __captures. get_all( #name_str)
956- . into_iter( )
957- . map( yeast:: NodeRef )
958- . collect( ) ;
959- } ,
960- CaptureMultiplicity :: Optional => quote ! {
961- let #name: Option <yeast:: NodeRef > =
962- __captures. get_opt( #name_str) . map( yeast:: NodeRef ) ;
963- } ,
964- CaptureMultiplicity :: Single => quote ! {
965- let #name: yeast:: NodeRef =
966- yeast:: NodeRef ( __captures. get_var( #name_str) . unwrap( ) ) ;
967- } ,
968- }
969- } )
970- . collect ( ) ;
971-
972- // Consume the body block.
973- let body_group = match tokens. next ( ) {
974- Some ( TokenTree :: Group ( g) ) if g. delimiter ( ) == Delimiter :: Brace => g,
975- other => {
976- return Err ( syn:: Error :: new (
977- Span :: call_site ( ) ,
978- format ! (
979- "expected a Rust block `{{ ... }}` after the query in manual_rule!, found: {other:?}"
980- ) ,
981- ) )
982- }
983- } ;
984- let body_stream = body_group. stream ( ) ;
985-
986- // No tokens should follow the body.
987- if let Some ( tok) = tokens. next ( ) {
988- return Err ( syn:: Error :: new_spanned (
989- tok,
990- "unexpected token after manual_rule! body" ,
991- ) ) ;
992- }
993-
994- Ok ( quote ! {
995- {
996- let __query = #query_code;
997- yeast:: Rule :: new( __query, Box :: new( |__ast: & mut yeast:: Ast , __captures: yeast:: captures:: Captures , __fresh: & yeast:: tree_builder:: FreshScope , __source_range: Option <tree_sitter:: Range >, __user_ctx: & mut _, __translator: yeast:: TranslatorHandle <' _, _>| {
998- // No auto-translate prefix for manual rules — the body
999- // is responsible for translating captures explicitly.
1000- #( #bindings) *
1001- let mut #ctx_ident = yeast:: build:: BuildCtx :: with_translator( __ast, & __captures, __fresh, __source_range, __user_ctx, __translator) ;
1002- #body_stream
1003- } ) )
1004- }
1005- } )
1006- }
1007-
1008929// ---------------------------------------------------------------------------
1009930// Token utilities
1010931// ---------------------------------------------------------------------------
@@ -1013,6 +934,16 @@ fn peek_is_at(tokens: &mut Tokens) -> bool {
1013934 matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '@' )
1014935}
1015936
937+ /// Consume an `@` or `@@` capture marker and the following name ident.
938+ /// Caller has already verified `peek_is_at(tokens)`.
939+ fn consume_capture_marker ( tokens : & mut Tokens ) -> Result < Ident > {
940+ tokens. next ( ) ; // consume the first `@`
941+ if peek_is_at ( tokens) {
942+ tokens. next ( ) ; // consume the second `@` of `@@`
943+ }
944+ expect_ident ( tokens, "expected capture name after `@` or `@@`" )
945+ }
946+
1016947fn peek_is_literal ( tokens : & mut Tokens ) -> bool {
1017948 matches ! ( tokens. peek( ) , Some ( TokenTree :: Literal ( _) ) )
1018949}
@@ -1113,8 +1044,7 @@ fn expect_repetition(tokens: &mut Tokens) -> Result<TokenStream> {
11131044
11141045fn maybe_wrap_capture ( tokens : & mut Tokens , base : TokenStream ) -> Result < TokenStream > {
11151046 if peek_is_at ( tokens) {
1116- tokens. next ( ) ; // consume @
1117- let name = expect_ident ( tokens, "expected capture name after @" ) ?;
1047+ let name = consume_capture_marker ( tokens) ?;
11181048 let name_str = name. to_string ( ) ;
11191049 Ok ( quote ! {
11201050 yeast:: query:: QueryNode :: Capture {
@@ -1141,13 +1071,12 @@ fn maybe_wrap_repetition(tokens: &mut Tokens, single: TokenStream) -> Result<Tok
11411071 }
11421072}
11431073
1144- /// If `@name` follows a Repeated list element, wrap each child SingleNode
1145- /// inside the repetition with a Capture. This matches tree-sitter semantics
1146- /// where `(_)* @name` captures each matched node.
1074+ /// If `@name` (or `@@name`) follows a Repeated list element, wrap each
1075+ /// child SingleNode inside the repetition with a Capture. This matches
1076+ /// tree-sitter semantics where `(_)* @name` captures each matched node.
11471077fn maybe_wrap_list_capture ( tokens : & mut Tokens , elem : TokenStream ) -> Result < TokenStream > {
11481078 if peek_is_at ( tokens) {
1149- tokens. next ( ) ;
1150- let name = expect_ident ( tokens, "expected capture name after @" ) ?;
1079+ let name = consume_capture_marker ( tokens) ?;
11511080 let name_str = name. to_string ( ) ;
11521081 // Re-parse the element isn't practical, so we generate a wrapper
11531082 // that creates a new Repeated with each child wrapped in a capture.
0 commit comments