@@ -617,6 +617,76 @@ fn extract_captures_inner(
617617 }
618618}
619619
620+ /// A rule's return-type annotation, when the body is a Rust block. Written
621+ /// between `=>` and the block body using the schema's own vocabulary:
622+ ///
623+ /// ```text
624+ /// => kind { … } // single node of that kind
625+ /// => kind? { … } // Option<KindId> (0 or 1)
626+ /// => kind* { … } // Vec<KindId> (0+)
627+ /// ```
628+ ///
629+ /// Template bodies (`=> (kind …)`) never carry an annotation — the
630+ /// output kind is the template root. The shorthand `=> kind` (no
631+ /// body) also carries no annotation. See `parse_rule_top` for dispatch.
632+ #[ derive( Clone , Debug ) ]
633+ struct ReturnAnnotation {
634+ kind : Ident ,
635+ multiplicity : AnnotationMultiplicity ,
636+ }
637+
638+ #[ derive( Clone , Copy , Debug , PartialEq ) ]
639+ enum AnnotationMultiplicity {
640+ Single ,
641+ Optional ,
642+ Repeated ,
643+ }
644+
645+ /// Peek at the token stream to decide whether the transform following
646+ /// `=>` is a **new** annotation form (`kind [? | *] { … }`). If so,
647+ /// consume the annotation and return it, leaving the `{ … }` body in
648+ /// the stream for the caller to parse. Otherwise leave the stream
649+ /// untouched and return `None`.
650+ ///
651+ /// The lookahead distinguishes:
652+ /// `kind {` → annotation (single)
653+ /// `kind? {` → annotation (optional)
654+ /// `kind* {` → annotation (repeated)
655+ /// `kind` → shorthand form (no `{` follows) — NOT an annotation
656+ /// anything else → template or bare block — NOT an annotation
657+ fn try_consume_return_annotation ( tokens : & mut Tokens ) -> Result < Option < ReturnAnnotation > > {
658+ // Must start with an identifier (the kind name).
659+ let mut lookahead = tokens. clone ( ) ;
660+ let Some ( TokenTree :: Ident ( _) ) = lookahead. next ( ) else {
661+ return Ok ( None ) ;
662+ } ;
663+ // Then optionally `?` or `*`, then a `{` group.
664+ let after_suffix = match lookahead. peek ( ) {
665+ Some ( TokenTree :: Punct ( p) ) if p. as_char ( ) == '?' || p. as_char ( ) == '*' => {
666+ lookahead. next ( ) ;
667+ lookahead. peek ( )
668+ }
669+ other => other,
670+ } ;
671+ if !matches ! ( after_suffix, Some ( TokenTree :: Group ( g) ) if g. delimiter( ) == Delimiter :: Brace ) {
672+ return Ok ( None ) ;
673+ }
674+ // Commit: consume the ident + suffix from the real stream.
675+ let kind = expect_ident ( tokens, "expected output-kind name in annotation" ) ?;
676+ let multiplicity = match tokens. peek ( ) {
677+ Some ( TokenTree :: Punct ( p) ) if p. as_char ( ) == '?' => {
678+ tokens. next ( ) ;
679+ AnnotationMultiplicity :: Optional
680+ }
681+ Some ( TokenTree :: Punct ( p) ) if p. as_char ( ) == '*' => {
682+ tokens. next ( ) ;
683+ AnnotationMultiplicity :: Repeated
684+ }
685+ _ => AnnotationMultiplicity :: Single ,
686+ } ;
687+ Ok ( Some ( ReturnAnnotation { kind, multiplicity } ) )
688+ }
689+
620690/// Parse `rule!( query => transform )`.
621691pub fn parse_rule_top ( input : TokenStream ) -> Result < TokenStream > {
622692 let mut tokens = input. into_iter ( ) . peekable ( ) ;
@@ -688,8 +758,52 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
688758 } )
689759 . collect ( ) ;
690760
691- // Parse transform: either shorthand `=> kind_name` or full `=> (template ...)`
692- let transform_body = if peek_is_field ( & mut tokens) && {
761+ // Parse transform: the token(s) after `=>` fall into one of three
762+ // shapes, dispatched in order:
763+ //
764+ // 1. `kind [? | *] { rust_body }` — annotated Rust body (NEW).
765+ // Static-analysis-ready: the annotation declares the output
766+ // kind and multiplicity in the schema's own vocabulary.
767+ // 2. `kind` alone — shorthand: emit `(kind field: {@cap})…` from
768+ // the query's captures.
769+ // 3. anything else — full template form (`(kind …)` or bare
770+ // `{ … }` splice via `parse_direct_list`).
771+ let annotation = try_consume_return_annotation ( & mut tokens) ?;
772+
773+ let transform_body = if let Some ( annotation) = annotation {
774+ // Annotation form: `=> kind [? | *] { rust_body }`.
775+ let body_group = expect_group ( & mut tokens, Delimiter :: Brace ) ?;
776+ if let Some ( tok) = tokens. next ( ) {
777+ return Err ( syn:: Error :: new_spanned (
778+ tok,
779+ "unexpected token after annotated rule body" ,
780+ ) ) ;
781+ }
782+ let body = body_group. stream ( ) ;
783+ // The annotation is not yet consumed by codegen — it will drive
784+ // typed handles once the schema-driven codegen lands. For now,
785+ // emit a self-documenting reference to the annotated kind and
786+ // preserve today's `Vec<yeast::Id>` closure return so behavior
787+ // is unchanged.
788+ let kind_str = annotation. kind . to_string ( ) ;
789+ let mult_str = match annotation. multiplicity {
790+ AnnotationMultiplicity :: Single => "single" ,
791+ AnnotationMultiplicity :: Optional => "optional" ,
792+ AnnotationMultiplicity :: Repeated => "repeated" ,
793+ } ;
794+ let _ = ( kind_str, mult_str) ; // silence unused warnings until wired
795+
796+ // For now, adapt the user's typed return value to the framework's
797+ // `Vec<yeast::Id>` closure result. This uses `IntoFieldIds`, which
798+ // already accepts a bare `Id`, an iterable of ids, or `Option<Id>`
799+ // — matching the three annotation multiplicities.
800+ quote ! {
801+ let __value = { #body } ;
802+ let mut __ids: Vec <yeast:: Id > = Vec :: new( ) ;
803+ yeast:: IntoFieldIds :: extend_into( __value, & mut __ids) ;
804+ __ids
805+ }
806+ } else if peek_is_field ( & mut tokens) && {
693807 // Shorthand form: bare identifier = output node kind.
694808 // Auto-generate template from captures.
695809 let mut lookahead = tokens. clone ( ) ;
@@ -749,6 +863,26 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
749863 vec![ __id]
750864 }
751865 } else {
866+ // Reject bare `{ ... }` transforms — they used to be accepted
867+ // as either a Rust body producing a `Vec<Id>` or a template
868+ // consisting of a single `{cap}` splice. Both patterns lost
869+ // static-analysis information (no visible output kind), so we
870+ // now require rules with block bodies to use the annotation
871+ // form `=> kind [? | *] { ... }`. Templates must start with a
872+ // parenthesized node (e.g. `(if_expr ...)`).
873+ if let Some ( TokenTree :: Group ( g) ) = tokens. peek ( ) {
874+ if g. delimiter ( ) == Delimiter :: Brace {
875+ let span = g. span ( ) ;
876+ return Err ( syn:: Error :: new (
877+ span,
878+ "bare `{...}` rule bodies are no longer accepted; \
879+ use the annotation form `=> kind [? | *] { ... }` \
880+ (where the kind names the output node's schema kind, \
881+ optionally suffixed with `?` or `*` for multiplicity)",
882+ ) ) ;
883+ }
884+ }
885+
752886 // Full template form
753887 let transform_items = parse_direct_list ( & mut tokens, & ctx_ident) ?;
754888
@@ -1026,10 +1160,7 @@ struct NamedString {
10261160}
10271161
10281162fn parse_named_string_arg ( tokens : & mut Tokens , expected_name : & str ) -> Result < NamedString > {
1029- let name = expect_ident (
1030- tokens,
1031- & format ! ( "expected `{expected_name}:` argument" ) ,
1032- ) ?;
1163+ let name = expect_ident ( tokens, & format ! ( "expected `{expected_name}:` argument" ) ) ?;
10331164 if name != expected_name {
10341165 return Err ( syn:: Error :: new_spanned (
10351166 name,
0 commit comments