Skip to content

Commit 636f31d

Browse files
committed
yeast: Track child source ranges in separate contexts
NB: This temporarily changes the test output for bulk imports, blocks, and things like synthesised Array and Option literals. Later commits will fix these up again.
1 parent 39547c7 commit 636f31d

73 files changed

Lines changed: 677 additions & 311 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎shared/yeast-macros/src/parse.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,7 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
987987
#(#translated_bindings)*
988988
let mut #ctx_ident = yeast::build::BuildCtx::with_translator(__ast, &__captures, __fresh, __source_range, __user_ctx, __translator);
989989
let __result: Vec<yeast::Id> = { #transform_body };
990+
let __result = #ctx_ident.finish_rule(__result);
990991
Ok(__result)
991992
}),
992993
)

‎shared/yeast/doc/yeast.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,27 @@ yeast::trees!(ctx,
235235
(identifier #{name}) // an identifier from a Rust variable
236236
```
237237

238+
### Source locations
239+
240+
Captured nodes keep the locations assigned by their own translations. New
241+
nodes in an output template derive their locations from their children. After
242+
the transform completes, the matched input node's range is added only to
243+
locally-created nodes returned as rule results:
244+
245+
```rust
246+
rule!(
247+
(wrapper child: (_) @child)
248+
=>
249+
(outer nested: (inner value: {child}))
250+
)
251+
```
252+
253+
Here `inner` derives its range from `child`, while the returned `outer` node
254+
also includes the full `wrapper` range. This lets replacement roots include
255+
elided keywords or delimiters without assigning the same broad range to every
256+
synthetic descendant. A transform that simply returns a translated capture
257+
does not widen that capture to the wrapper's range.
258+
238259
For reviewing locations, `DumpOptions::show_abridged_source` prints each node's
239260
source range with every direct child replaced by its field name in Unicode
240261
angle brackets. This keeps delimiters and other parent-owned syntax visible

‎shared/yeast/src/build.rs‎

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::collections::BTreeMap;
1+
use std::collections::{BTreeMap, BTreeSet};
22

33
use crate::captures::Captures;
44
use 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,22 @@ 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.
37-
pub source_range: Option<Range>,
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.
41+
pub matched_source_range: Option<Range>,
3842
/// User-supplied context, accessible directly via `ctx.field` (via Deref).
3943
pub user_ctx: &'a mut C,
4044
/// Optional translator handle, populated when the context is built by
4145
/// the framework's rule driver. None when the context is built by hand.
4246
pub(crate) translator: Option<TranslatorHandle<'a, C>>,
47+
/// Nodes built directly through this context without an explicit source
48+
/// range in either their content or constructor argument. Recursive
49+
/// translations use their own context and therefore do not contribute to
50+
/// this list.
51+
created_nodes: BTreeSet<Id>,
4352
}
4453

4554
impl<'a, C> BuildCtx<'a, C> {
@@ -53,26 +62,10 @@ impl<'a, C> BuildCtx<'a, C> {
5362
ast,
5463
captures,
5564
fresh,
56-
source_range: None,
57-
user_ctx,
58-
translator: None,
59-
}
60-
}
61-
62-
pub fn with_source_range(
63-
ast: &'a mut Ast,
64-
captures: &'a Captures,
65-
fresh: &'a FreshScope,
66-
source_range: Option<Range>,
67-
user_ctx: &'a mut C,
68-
) -> Self {
69-
Self {
70-
ast,
71-
captures,
72-
fresh,
73-
source_range,
65+
matched_source_range: None,
7466
user_ctx,
7567
translator: None,
68+
created_nodes: BTreeSet::new(),
7669
}
7770
}
7871

@@ -90,12 +83,80 @@ impl<'a, C> BuildCtx<'a, C> {
9083
ast,
9184
captures,
9285
fresh,
93-
source_range,
86+
matched_source_range: source_range,
9487
user_ctx,
9588
translator: Some(translator),
89+
created_nodes: BTreeSet::new(),
9690
}
9791
}
9892

93+
/// Create a node and record it as constructed by this rule invocation.
94+
pub fn create_node_with_range(
95+
&mut self,
96+
kind: KindId,
97+
content: NodeContent,
98+
fields: BTreeMap<FieldId, Vec<Id>>,
99+
is_named: bool,
100+
source_range: Option<Range>,
101+
) -> Id {
102+
let has_explicit_source_range =
103+
source_range.is_some() || matches!(&content, NodeContent::Range(_));
104+
let id = self
105+
.ast
106+
.create_node_with_range(kind, content, fields, is_named, source_range);
107+
if !has_explicit_source_range {
108+
self.created_nodes.insert(id);
109+
}
110+
id
111+
}
112+
113+
/// Create a node using this context's explicit default source range.
114+
pub fn create_node(
115+
&mut self,
116+
kind: KindId,
117+
content: NodeContent,
118+
fields: BTreeMap<FieldId, Vec<Id>>,
119+
is_named: bool,
120+
) -> Id {
121+
self.create_node_with_range(kind, content, fields, is_named, None)
122+
}
123+
124+
/// Create a named token and record it as constructed by this rule invocation.
125+
pub fn create_named_token_with_range(
126+
&mut self,
127+
kind: &'static str,
128+
content: String,
129+
source_range: Option<Range>,
130+
) -> Id {
131+
let has_explicit_source_range = source_range.is_some();
132+
let id = self
133+
.ast
134+
.create_named_token_with_range(kind, content, source_range);
135+
if !has_explicit_source_range {
136+
self.created_nodes.insert(id);
137+
}
138+
id
139+
}
140+
141+
/// Create a named token using this context's explicit default source range.
142+
pub fn create_named_token(&mut self, kind: &'static str, content: String) -> Id {
143+
self.create_named_token_with_range(kind, content, None)
144+
}
145+
146+
/// Finish the current rule invocation by applying the matched source range
147+
/// to locally-created result roots.
148+
#[doc(hidden)]
149+
pub fn finish_rule(self, results: Vec<Id>) -> Vec<Id> {
150+
if let Some(source_range) = self.matched_source_range {
151+
for &id in &results {
152+
if self.created_nodes.contains(&id) {
153+
self.ast.extend_source_range(id, source_range);
154+
}
155+
}
156+
}
157+
results
158+
}
159+
99160
/// Look up a capture variable, returning its node Id.
100161
pub fn capture(&self, name: &str) -> Id {
101162
self.captures
@@ -133,41 +194,33 @@ impl<'a, C> BuildCtx<'a, C> {
133194
.unwrap_or_else(|| panic!("build: field '{name}' not found"));
134195
field_map.entry(field_id).or_default().extend(ids);
135196
}
136-
self.ast.create_node_with_range(
197+
self.create_node(
137198
kind_id,
138199
NodeContent::DynamicString(String::new()),
139200
field_map,
140201
true,
141-
self.source_range,
142202
)
143203
}
144204

145205
/// Create a leaf node with a fixed string content.
146206
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)
207+
self.create_named_token(kind, value.to_string())
149208
}
150209

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.
210+
/// Create a leaf node with fixed content and an optional source range.
153211
pub fn literal_with_source_range(
154212
&mut self,
155213
kind: &'static str,
156214
value: &str,
157215
source_range: Option<Range>,
158216
) -> Id {
159-
self.ast.create_named_token_with_range(
160-
kind,
161-
value.to_string(),
162-
source_range.or(self.source_range),
163-
)
217+
self.create_named_token_with_range(kind, value.to_string(), source_range)
164218
}
165219

166220
/// Create a leaf node with an auto-generated unique name.
167221
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
168222
let generated = self.fresh.resolve(name);
169-
self.ast
170-
.create_named_token_with_range(kind, generated, self.source_range)
223+
self.create_named_token(kind, generated)
171224
}
172225
}
173226

@@ -203,8 +256,9 @@ impl<C: Clone> BuildCtx<'_, C> {
203256

204257
/// Run `f` with a temporary child [`BuildCtx`] whose `user_ctx` is
205258
/// 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`
259+
/// (`ast`, `captures`, `fresh`, source ranges, `translator`) by re-borrow.
260+
/// Nodes constructed through the child remain part of the current rule
261+
/// invocation. Any mutations `f` makes to the child's `user_ctx`
208262
/// are discarded when it returns — no restore needed, because the
209263
/// mutations only ever happened on a local clone.
210264
///
@@ -235,11 +289,16 @@ impl<C: Clone> BuildCtx<'_, C> {
235289
ast: &mut *self.ast,
236290
captures: self.captures,
237291
fresh: self.fresh,
238-
source_range: self.source_range,
292+
matched_source_range: self.matched_source_range,
239293
user_ctx: &mut child_user_ctx,
240294
translator: self.translator,
295+
created_nodes: BTreeSet::new(),
241296
};
242-
f(&mut child)
297+
let result = f(&mut child);
298+
let created_nodes = std::mem::take(&mut child.created_nodes);
299+
drop(child);
300+
self.created_nodes.extend(created_nodes);
301+
result
243302
// child_user_ctx dropped; the outer `self` is unaffected.
244303
}
245304
}

‎shared/yeast/src/lib.rs‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ pub trait YeastDisplay {
128128

129129
/// Optional source range for values used in `#{expr}` interpolations.
130130
///
131-
/// By default this returns `None`, so synthesized leaves inherit the matched
132-
/// rule's source range. `Id` returns the referenced node's range, letting
133-
/// `(kind #{capture})` carry the captured node's location.
131+
/// By default this returns `None`, so synthesized leaves use the current
132+
/// [`crate::build::BuildCtx`] default range, if any. `Id` returns the
133+
/// referenced node's range, letting `(kind #{capture})` carry the captured
134+
/// node's location.
134135
pub trait YeastSourceRange {
135136
fn yeast_source_range(&self, ast: &Ast) -> Option<Range>;
136137
}
@@ -592,8 +593,7 @@ impl Ast {
592593
// Parsed nodes already carry an exact source range in their content.
593594
NodeContent::Range(_) => source_range,
594595
// Synthesized nodes derive location from both their children and
595-
// the inherited rule-match range, so tokens matched by a rule but
596-
// elided from its output still contribute to the replacement range.
596+
// any explicitly supplied source range.
597597
_ => self
598598
.union_source_range_of_children(&fields)
599599
.map_or(source_range, |child_range| {
@@ -618,6 +618,24 @@ impl Ast {
618618
Id(id)
619619
}
620620

621+
/// Extend a synthetic node's source range to include `source_range`.
622+
///
623+
/// Parsed nodes carry their exact range in [`NodeContent::Range`] and must
624+
/// not be modified through this API.
625+
pub fn extend_source_range(&mut self, id: Id, source_range: Range) {
626+
let node = self
627+
.nodes
628+
.get_mut(id.0)
629+
.unwrap_or_else(|| panic!("extend_source_range: invalid node id {}", id.0));
630+
if matches!(node.content, NodeContent::Range(_)) {
631+
panic!("extend_source_range: cannot modify a parsed node");
632+
}
633+
node.source_range = Some(match node.source_range {
634+
Some(existing) => union_source_ranges(existing, source_range),
635+
None => source_range,
636+
});
637+
}
638+
621639
/// Register a named node kind, returning its id (idempotent). Lets callers
622640
/// build an AST in a single pass, registering kinds as nodes are created
623641
/// rather than pre-populating the schema.
@@ -1134,8 +1152,8 @@ impl<C> Rule<C> {
11341152
}
11351153
}
11361154

1137-
/// Run this rule's transform with the given captures, using `node`'s
1138-
/// source range as the source range of the produced nodes.
1155+
/// Run this rule's transform with the given captures, making `node`'s
1156+
/// source range available to the transform.
11391157
fn run_transform(
11401158
&self,
11411159
ast: &mut Ast,

0 commit comments

Comments
 (0)