1- use std:: collections:: BTreeMap ;
1+ use std:: collections:: { BTreeMap , BTreeSet } ;
22
33use crate :: captures:: Captures ;
44use crate :: tree_builder:: FreshScope ;
5- use crate :: { Ast , FieldId , Id , NodeContent , Range , TranslatorHandle } ;
5+ use crate :: { Ast , FieldId , Id , KindId , NodeContent , Range , TranslatorHandle } ;
66
77/// Context for building new AST nodes during a transformation.
88///
@@ -33,13 +33,24 @@ pub struct BuildCtx<'a, C: 'a = ()> {
3333 pub ast : & ' a mut Ast ,
3434 pub captures : & ' a Captures ,
3535 pub fresh : & ' a FreshScope ,
36- /// Source range of the matched node, inherited by synthetic nodes.
36+ /// Source range of the node matched by the current rule.
37+ ///
38+ /// The `rule!` macro applies this range to locally-created result roots
39+ /// after the transform completes. Nested synthetic nodes derive their
40+ /// ranges from their children, falling back to an empty range at this
41+ /// range's start.
3742 pub source_range : Option < Range > ,
3843 /// User-supplied context, accessible directly via `ctx.field` (via Deref).
3944 pub user_ctx : & ' a mut C ,
4045 /// Optional translator handle, populated when the context is built by
4146 /// the framework's rule driver. None when the context is built by hand.
4247 pub ( crate ) translator : Option < TranslatorHandle < ' a , C > > ,
48+ /// Nodes built directly through this context without an explicit source
49+ /// range in either their content or constructor argument. Recursive
50+ /// translations use their own context and therefore do not contribute to
51+ /// this set. Membership checks identify result roots to widen, while
52+ /// removals exclude nodes that are later assigned an explicit range.
53+ created_nodes_without_source_range : BTreeSet < Id > ,
4354}
4455
4556impl < ' a , C > BuildCtx < ' a , C > {
@@ -56,9 +67,11 @@ impl<'a, C> BuildCtx<'a, C> {
5667 source_range : None ,
5768 user_ctx,
5869 translator : None ,
70+ created_nodes_without_source_range : BTreeSet :: new ( ) ,
5971 }
6072 }
6173
74+ /// Construct a context carrying a matched input source range.
6275 pub fn with_source_range (
6376 ast : & ' a mut Ast ,
6477 captures : & ' a Captures ,
@@ -73,6 +86,7 @@ impl<'a, C> BuildCtx<'a, C> {
7386 source_range,
7487 user_ctx,
7588 translator : None ,
89+ created_nodes_without_source_range : BTreeSet :: new ( ) ,
7690 }
7791 }
7892
@@ -93,7 +107,81 @@ impl<'a, C> BuildCtx<'a, C> {
93107 source_range,
94108 user_ctx,
95109 translator : Some ( translator) ,
110+ created_nodes_without_source_range : BTreeSet :: new ( ) ,
111+ }
112+ }
113+
114+ /// Create a node and record it as constructed by this rule invocation.
115+ pub fn create_node_with_range (
116+ & mut self ,
117+ kind : KindId ,
118+ content : NodeContent ,
119+ fields : BTreeMap < FieldId , Vec < Id > > ,
120+ is_named : bool ,
121+ source_range : Option < Range > ,
122+ ) -> Id {
123+ let has_explicit_source_range =
124+ source_range. is_some ( ) || matches ! ( & content, NodeContent :: Range ( _) ) ;
125+ let id = self
126+ . ast
127+ . create_node_with_range ( kind, content, fields, is_named, source_range) ;
128+ if self
129+ . ast
130+ . get_node ( id)
131+ . is_some_and ( |node| node. source_range ( ) . is_none ( ) )
132+ {
133+ if let Some ( source_range) = self . source_range {
134+ self . ast
135+ . extend_source_range ( id, source_range. empty_at_start ( ) ) ;
136+ }
137+ }
138+ if !has_explicit_source_range {
139+ self . created_nodes_without_source_range . insert ( id) ;
96140 }
141+ id
142+ }
143+
144+ /// Create a named token and record it as constructed by this rule invocation.
145+ pub fn create_named_token_with_range (
146+ & mut self ,
147+ kind : & ' static str ,
148+ content : String ,
149+ source_range : Option < Range > ,
150+ ) -> Id {
151+ let has_explicit_source_range = source_range. is_some ( ) ;
152+ let source_range =
153+ source_range. or_else ( || self . source_range . map ( |range| range. empty_at_start ( ) ) ) ;
154+ let id = self
155+ . ast
156+ . create_named_token_with_range ( kind, content, source_range) ;
157+ if !has_explicit_source_range {
158+ self . created_nodes_without_source_range . insert ( id) ;
159+ }
160+ id
161+ }
162+
163+ /// Finish the current rule invocation by applying the matched source range
164+ /// to locally-created result roots.
165+ #[ doc( hidden) ]
166+ pub fn finish_rule ( self , results : Vec < Id > ) -> Vec < Id > {
167+ if let Some ( source_range) = self . source_range {
168+ for & id in & results {
169+ if self . created_nodes_without_source_range . contains ( & id) {
170+ self . ast . extend_source_range ( id, source_range) ;
171+ }
172+ }
173+ }
174+ results
175+ }
176+
177+ /// Assign an explicit source range to a newly-built result root.
178+ #[ doc( hidden) ]
179+ pub fn set_node_source_range ( & mut self , node : Id , source_range : Option < Range > ) -> Id {
180+ if let Some ( source_range) = source_range {
181+ self . ast . set_source_range ( node, source_range) ;
182+ self . created_nodes_without_source_range . remove ( & node) ;
183+ }
184+ node
97185 }
98186
99187 /// Look up a capture variable, returning its node Id.
@@ -119,6 +207,18 @@ impl<'a, C> BuildCtx<'a, C> {
119207 self . ast . source_text ( id)
120208 }
121209
210+ /// Return the source range of a parsed or synthetic node.
211+ fn source_range_of ( & self , id : Id ) -> Option < Range > {
212+ self . ast . get_node ( id) . and_then ( |node| node. source_range ( ) )
213+ }
214+
215+ /// Return an empty range between two non-overlapping nodes.
216+ pub fn empty_source_range_between ( & self , left : Id , right : Id ) -> Option < Range > {
217+ let left = self . source_range_of ( left) ?;
218+ let right = self . source_range_of ( right) ?;
219+ ( left. end_byte <= right. start_byte ) . then ( || left. empty_at_end ( ) )
220+ }
221+
122222 /// Create a named AST node with the given kind and fields.
123223 pub fn node ( & mut self , kind : & str , fields : Vec < ( & str , Vec < Id > ) > ) -> Id {
124224 let kind_id = self
@@ -133,41 +233,40 @@ impl<'a, C> BuildCtx<'a, C> {
133233 . unwrap_or_else ( || panic ! ( "build: field '{name}' not found" ) ) ;
134234 field_map. entry ( field_id) . or_default ( ) . extend ( ids) ;
135235 }
136- self . ast . create_node_with_range (
236+ self . create_node_with_range (
137237 kind_id,
138238 NodeContent :: DynamicString ( String :: new ( ) ) ,
139239 field_map,
140240 true ,
141- self . source_range ,
241+ None ,
142242 )
143243 }
144244
145245 /// Create a leaf node with a fixed string content.
146246 pub fn literal ( & mut self , kind : & ' static str , value : & str ) -> Id {
147- self . ast
148- . create_named_token_with_range ( kind, value. to_string ( ) , self . source_range )
247+ self . create_named_token_with_range ( kind, value. to_string ( ) , None )
149248 }
150249
151- /// Create a leaf node with fixed content and an optional preferred source range.
152- /// If `source_range` is `None`, falls back to this context's inherited range.
250+ /// Create a leaf node with fixed content and an optional source range.
153251 pub fn literal_with_source_range (
154252 & mut self ,
155253 kind : & ' static str ,
156254 value : & str ,
157255 source_range : Option < Range > ,
158256 ) -> Id {
159- self . ast . create_named_token_with_range (
160- kind,
161- value. to_string ( ) ,
162- source_range. or ( self . source_range ) ,
163- )
257+ self . create_named_token_with_range ( kind, value. to_string ( ) , source_range)
258+ }
259+
260+ /// Create a literal with an empty range at another node's start.
261+ pub fn literal_at_start_of ( & mut self , kind : & ' static str , value : & str , source : Id ) -> Id {
262+ let source_range = self . source_range_of ( source) . map ( Range :: empty_at_start) ;
263+ self . literal_with_source_range ( kind, value, source_range)
164264 }
165265
166266 /// Create a leaf node with an auto-generated unique name.
167267 pub fn fresh ( & mut self , kind : & ' static str , name : & str ) -> Id {
168268 let generated = self . fresh . resolve ( name) ;
169- self . ast
170- . create_named_token_with_range ( kind, generated, self . source_range )
269+ self . create_named_token_with_range ( kind, generated, None )
171270 }
172271}
173272
@@ -203,8 +302,9 @@ impl<C: Clone> BuildCtx<'_, C> {
203302
204303 /// Run `f` with a temporary child [`BuildCtx`] whose `user_ctx` is
205304 /// a fresh clone of the current one, sharing everything else
206- /// (`ast`, `captures`, `fresh`, `source_range`, `translator`) by
207- /// re-borrow. Any mutations `f` makes to the child's `user_ctx`
305+ /// (`ast`, `captures`, `fresh`, source ranges, `translator`) by re-borrow.
306+ /// Nodes constructed through the child remain part of the current rule
307+ /// invocation. Any mutations `f` makes to the child's `user_ctx`
208308 /// are discarded when it returns — no restore needed, because the
209309 /// mutations only ever happened on a local clone.
210310 ///
@@ -238,8 +338,15 @@ impl<C: Clone> BuildCtx<'_, C> {
238338 source_range : self . source_range ,
239339 user_ctx : & mut child_user_ctx,
240340 translator : self . translator ,
341+ created_nodes_without_source_range : BTreeSet :: new ( ) ,
241342 } ;
242- f ( & mut child)
343+ let result = f ( & mut child) ;
344+ let created_nodes_without_source_range =
345+ std:: mem:: take ( & mut child. created_nodes_without_source_range ) ;
346+ drop ( child) ;
347+ self . created_nodes_without_source_range
348+ . extend ( created_nodes_without_source_range) ;
349+ result
243350 // child_user_ctx dropped; the outer `self` is unaffected.
244351 }
245352}
0 commit comments