Skip to content

Commit e10de64

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 565b133 commit e10de64

19 files changed

Lines changed: 499 additions & 44 deletions

‎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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,36 @@ 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+
259+
Rust bodies can explicitly assign a default range to a synthetic subtree whose
260+
location cannot be derived from children:
261+
262+
```rust
263+
let synthetic = ctx.with_node_source_range(ctx.matched_source_range, |ctx| {
264+
tree!(ctx, (synthetic_node))
265+
});
266+
```
267+
238268
### Optional fields (`?`)
239269

240270
A `?` on a field's value makes that field fallible. If a `#{expr}` anywhere

‎shared/yeast/src/build.rs‎

Lines changed: 120 additions & 15 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,25 @@ 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+
/// Optional source range explicitly inherited by every synthetic node built
37+
/// through this context.
3738
pub source_range: Option<Range>,
39+
/// Source range of the node matched by the current rule.
40+
///
41+
/// The `rule!` macro applies this range to locally-created result roots
42+
/// after the transform completes. Nested synthetic nodes derive their
43+
/// ranges from their children unless `source_range` is explicitly set.
44+
pub matched_source_range: Option<Range>,
3845
/// User-supplied context, accessible directly via `ctx.field` (via Deref).
3946
pub user_ctx: &'a mut C,
4047
/// Optional translator handle, populated when the context is built by
4148
/// the framework's rule driver. None when the context is built by hand.
4249
pub(crate) translator: Option<TranslatorHandle<'a, C>>,
50+
/// Nodes built directly through this context without an explicit source
51+
/// range in either their content or constructor argument. Recursive
52+
/// translations use their own context and therefore do not contribute to
53+
/// this list.
54+
created_nodes: BTreeSet<Id>,
4355
}
4456

4557
impl<'a, C> BuildCtx<'a, C> {
@@ -54,8 +66,10 @@ impl<'a, C> BuildCtx<'a, C> {
5466
captures,
5567
fresh,
5668
source_range: None,
69+
matched_source_range: None,
5770
user_ctx,
5871
translator: None,
72+
created_nodes: BTreeSet::new(),
5973
}
6074
}
6175

@@ -71,8 +85,10 @@ impl<'a, C> BuildCtx<'a, C> {
7185
captures,
7286
fresh,
7387
source_range,
88+
matched_source_range: None,
7489
user_ctx,
7590
translator: None,
91+
created_nodes: BTreeSet::new(),
7692
}
7793
}
7894

@@ -90,10 +106,94 @@ impl<'a, C> BuildCtx<'a, C> {
90106
ast,
91107
captures,
92108
fresh,
93-
source_range,
109+
source_range: None,
110+
matched_source_range: source_range,
94111
user_ctx,
95112
translator: Some(translator),
113+
created_nodes: BTreeSet::new(),
114+
}
115+
}
116+
117+
/// Create a node and record it as constructed by this rule invocation.
118+
pub fn create_node_with_range(
119+
&mut self,
120+
kind: KindId,
121+
content: NodeContent,
122+
fields: BTreeMap<FieldId, Vec<Id>>,
123+
is_named: bool,
124+
source_range: Option<Range>,
125+
) -> Id {
126+
let has_explicit_source_range =
127+
source_range.is_some() || matches!(&content, NodeContent::Range(_));
128+
let id = self
129+
.ast
130+
.create_node_with_range(kind, content, fields, is_named, source_range);
131+
if !has_explicit_source_range {
132+
self.created_nodes.insert(id);
133+
}
134+
id
135+
}
136+
137+
/// Create a node using this context's explicit default source range.
138+
pub fn create_node(
139+
&mut self,
140+
kind: KindId,
141+
content: NodeContent,
142+
fields: BTreeMap<FieldId, Vec<Id>>,
143+
is_named: bool,
144+
) -> Id {
145+
self.create_node_with_range(kind, content, fields, is_named, self.source_range)
146+
}
147+
148+
/// Create a named token and record it as constructed by this rule invocation.
149+
pub fn create_named_token_with_range(
150+
&mut self,
151+
kind: &'static str,
152+
content: String,
153+
source_range: Option<Range>,
154+
) -> Id {
155+
let has_explicit_source_range = source_range.is_some();
156+
let id = self
157+
.ast
158+
.create_named_token_with_range(kind, content, source_range);
159+
if !has_explicit_source_range {
160+
self.created_nodes.insert(id);
161+
}
162+
id
163+
}
164+
165+
/// Create a named token using this context's explicit default source range.
166+
pub fn create_named_token(&mut self, kind: &'static str, content: String) -> Id {
167+
self.create_named_token_with_range(kind, content, self.source_range)
168+
}
169+
170+
/// Finish the current rule invocation by applying the matched source range
171+
/// to locally-created result roots.
172+
#[doc(hidden)]
173+
pub fn finish_rule(self, results: Vec<Id>) -> Vec<Id> {
174+
if let Some(source_range) = self.matched_source_range {
175+
for &id in &results {
176+
if self.created_nodes.contains(&id) {
177+
self.ast.extend_source_range(id, source_range);
178+
}
179+
}
96180
}
181+
results
182+
}
183+
184+
/// Build nodes with an explicit default source range.
185+
///
186+
/// This is the Rust escape hatch for synthetic subtrees whose useful
187+
/// location cannot be derived from children. The previous default is
188+
/// restored after `f` returns.
189+
pub fn with_node_source_range<F, R>(&mut self, source_range: Option<Range>, f: F) -> R
190+
where
191+
F: FnOnce(&mut Self) -> R,
192+
{
193+
let previous = std::mem::replace(&mut self.source_range, source_range);
194+
let result = f(self);
195+
self.source_range = previous;
196+
result
97197
}
98198

99199
/// Look up a capture variable, returning its node Id.
@@ -133,30 +233,29 @@ 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(
137237
kind_id,
138238
NodeContent::DynamicString(String::new()),
139239
field_map,
140240
true,
141-
self.source_range,
142241
)
143242
}
144243

145244
/// Create a leaf node with a fixed string content.
146245
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)
246+
self.create_named_token(kind, value.to_string())
149247
}
150248

151249
/// 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+
/// If `source_range` is `None`, falls back to this context's explicit
251+
/// default range.
153252
pub fn literal_with_source_range(
154253
&mut self,
155254
kind: &'static str,
156255
value: &str,
157256
source_range: Option<Range>,
158257
) -> Id {
159-
self.ast.create_named_token_with_range(
258+
self.create_named_token_with_range(
160259
kind,
161260
value.to_string(),
162261
source_range.or(self.source_range),
@@ -166,8 +265,7 @@ impl<'a, C> BuildCtx<'a, C> {
166265
/// Create a leaf node with an auto-generated unique name.
167266
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
168267
let generated = self.fresh.resolve(name);
169-
self.ast
170-
.create_named_token_with_range(kind, generated, self.source_range)
268+
self.create_named_token(kind, generated)
171269
}
172270
}
173271

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

204302
/// Run `f` with a temporary child [`BuildCtx`] whose `user_ctx` is
205303
/// 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`
304+
/// (`ast`, `captures`, `fresh`, source ranges, `translator`) by re-borrow.
305+
/// Nodes constructed through the child remain part of the current rule
306+
/// invocation. Any mutations `f` makes to the child's `user_ctx`
208307
/// are discarded when it returns — no restore needed, because the
209308
/// mutations only ever happened on a local clone.
210309
///
@@ -236,10 +335,16 @@ impl<C: Clone> BuildCtx<'_, C> {
236335
captures: self.captures,
237336
fresh: self.fresh,
238337
source_range: self.source_range,
338+
matched_source_range: self.matched_source_range,
239339
user_ctx: &mut child_user_ctx,
240340
translator: self.translator,
341+
created_nodes: BTreeSet::new(),
241342
};
242-
f(&mut child)
343+
let result = f(&mut child);
344+
let created_nodes = std::mem::take(&mut child.created_nodes);
345+
drop(child);
346+
self.created_nodes.extend(created_nodes);
347+
result
243348
// child_user_ctx dropped; the outer `self` is unaffected.
244349
}
245350
}

‎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)