Skip to content

Commit 1521355

Browse files
committed
yeast: Add explicit location macros
Adds two new macros, both variants of the existing `tree!` macro. First, `tree_at!` takes an extra argument, and sets the location of the root of the constructed tree to be that of the argument in question. Secondly, `tree_spanning!` does the same construction, but accepts an iterable of nodes instead. It then makes it so that the location of the root node of the constructed tree is the smallest span that contains all of the locations given by the iterable.
1 parent ffb408e commit 1521355

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

‎shared/yeast/doc/yeast.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,22 @@ elided keywords or delimiters without assigning the same broad range to every
256256
synthetic descendant. A transform that simply returns a translated capture
257257
does not widen that capture to the wrapper's range.
258258

259+
When the desired range belongs to another node, `tree_at!` assigns that range
260+
to the template's root. Nested nodes still derive their own locations normally:
261+
262+
```rust
263+
let synthetic = tree_at!(ctx, source, (synthetic_node child: (nested value: {child})));
264+
```
265+
266+
`tree_spanning!` similarly assigns the union of several node ranges:
267+
268+
```rust
269+
let synthetic = tree_spanning!(ctx, nodes, (synthetic_node child: {child}));
270+
```
271+
272+
For literals, `ctx.literal_at_start_of(...)` creates an empty range at another
273+
node's start.
274+
259275
For reviewing locations, `DumpOptions::show_abridged_source` prints each node's
260276
source range with every direct child replaced by its field name in Unicode
261277
angle brackets. This keeps delimiters and other parent-owned syntax visible

‎shared/yeast/src/build.rs‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ impl<'a, C> BuildCtx<'a, C> {
181181
results
182182
}
183183

184+
/// Assign an explicit source range to a newly-built result root.
185+
#[doc(hidden)]
186+
pub fn set_node_source_range(&mut self, node: Id, source_range: Option<Range>) -> Id {
187+
if let Some(source_range) = source_range {
188+
self.ast.set_source_range(node, source_range);
189+
self.created_nodes.remove(&node);
190+
}
191+
node
192+
}
193+
184194
/// Look up a capture variable, returning its node Id.
185195
pub fn capture(&self, name: &str) -> Id {
186196
self.captures
@@ -204,6 +214,18 @@ impl<'a, C> BuildCtx<'a, C> {
204214
self.ast.source_text(id)
205215
}
206216

217+
/// Return the source range of a parsed or synthetic node.
218+
fn source_range_of(&self, id: Id) -> Option<Range> {
219+
self.ast.get_node(id).and_then(|node| node.source_range())
220+
}
221+
222+
/// Return an empty range between two non-overlapping nodes.
223+
pub fn empty_source_range_between(&self, left: Id, right: Id) -> Option<Range> {
224+
let left = self.source_range_of(left)?;
225+
let right = self.source_range_of(right)?;
226+
(left.end_byte <= right.start_byte).then(|| left.empty_at_end())
227+
}
228+
207229
/// Create a named AST node with the given kind and fields.
208230
pub fn node(&mut self, kind: &str, fields: Vec<(&str, Vec<Id>)>) -> Id {
209231
let kind_id = self
@@ -231,9 +253,7 @@ impl<'a, C> BuildCtx<'a, C> {
231253
self.create_named_token(kind, value.to_string())
232254
}
233255

234-
/// Create a leaf node with fixed content and an optional preferred source range.
235-
/// If `source_range` is `None`, falls back to this context's explicit
236-
/// default range.
256+
/// Create a leaf node with fixed content and an optional source range.
237257
pub fn literal_with_source_range(
238258
&mut self,
239259
kind: &'static str,
@@ -247,6 +267,12 @@ impl<'a, C> BuildCtx<'a, C> {
247267
)
248268
}
249269

270+
/// Create a literal with an empty range at another node's start.
271+
pub fn literal_at_start_of(&mut self, kind: &'static str, value: &str, source: Id) -> Id {
272+
let source_range = self.source_range_of(source).map(Range::empty_at_start);
273+
self.literal_with_source_range(kind, value, source_range)
274+
}
275+
250276
/// Create a leaf node with an auto-generated unique name.
251277
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
252278
let generated = self.fresh.resolve(name);

‎shared/yeast/src/lib.rs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,40 @@ mod visitor;
1818
pub use range::{Point, Range};
1919
pub use yeast_macros::{query, rule, rules, tree, trees};
2020

21+
/// Build a single AST node whose root uses another node's source range.
22+
///
23+
/// Nested nodes in the template are built normally and derive their locations
24+
/// from their own children.
25+
#[macro_export]
26+
macro_rules! tree_at {
27+
($ctx:ident, $source:expr, ($($tree:tt)*)) => {{
28+
let __yeast_source: $crate::Id = $source;
29+
let __yeast_source_range = $ctx
30+
.ast
31+
.get_node(__yeast_source)
32+
.and_then(|node| node.source_range());
33+
let __yeast_node: $crate::Id = $crate::tree!($ctx, ($($tree)*));
34+
$ctx.set_node_source_range(__yeast_node, __yeast_source_range)
35+
}};
36+
}
37+
38+
/// Build a single AST node whose root spans a collection of nodes.
39+
///
40+
/// Nested nodes in the template are built normally and derive their locations
41+
/// from their own children.
42+
#[macro_export]
43+
macro_rules! tree_spanning {
44+
($ctx:ident, $sources:expr, ($($tree:tt)*)) => {{
45+
let __yeast_source_range = ::std::iter::IntoIterator::into_iter($sources)
46+
.filter_map(|source: $crate::Id| {
47+
$ctx.ast.get_node(source).and_then(|node| node.source_range())
48+
})
49+
.reduce($crate::Range::union);
50+
let __yeast_node: $crate::Id = $crate::tree!($ctx, ($($tree)*));
51+
$ctx.set_node_source_range(__yeast_node, __yeast_source_range)
52+
}};
53+
}
54+
2155
use captures::Captures;
2256
use query::QueryNode;
2357

@@ -633,6 +667,18 @@ impl Ast {
633667
});
634668
}
635669

670+
/// Replace a synthetic node's source range.
671+
pub(crate) fn set_source_range(&mut self, id: Id, source_range: Range) {
672+
let node = self
673+
.nodes
674+
.get_mut(id.0)
675+
.unwrap_or_else(|| panic!("set_source_range: invalid node id {}", id.0));
676+
if matches!(node.content, NodeContent::Range(_)) {
677+
panic!("set_source_range: cannot modify a parsed node");
678+
}
679+
node.source_range = Some(source_range);
680+
}
681+
636682
/// Register a named node kind, returning its id (idempotent). Lets callers
637683
/// build an AST in a single pass, registering kinds as nodes are created
638684
/// rather than pre-populating the schema.

‎shared/yeast/tests/test.rs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,77 @@ fn test_explicit_recursive_translation_keeps_nested_rule_location() {
17991799
assert_eq!(identifier.byte_range(), 4..7);
18001800
}
18011801

1802+
/// `tree_at!` assigns the captured node's range only to the template root.
1803+
#[test]
1804+
fn test_tree_at_assigns_capture_range_to_root_only() {
1805+
let rule: Rule = rule!(
1806+
(call
1807+
method: (identifier) @name
1808+
receiver: (identifier) @recv
1809+
) @@source
1810+
=>
1811+
call {
1812+
let arguments = tree_at!(ctx, source, (argument_list argument: (integer "0")));
1813+
tree!((call method: {name} receiver: {recv} arguments: {arguments}))
1814+
}
1815+
);
1816+
1817+
let ast = run_and_ast("foo.bar()", vec![rule]);
1818+
let arguments = ast
1819+
.reachable_node_ids()
1820+
.into_iter()
1821+
.filter_map(|id| ast.get_node(id))
1822+
.find(|node| node.kind_name() == "argument_list")
1823+
.expect("argument list exists");
1824+
assert_eq!(arguments.byte_range(), 0..9);
1825+
1826+
let integer = ast
1827+
.reachable_node_ids()
1828+
.into_iter()
1829+
.filter_map(|id| ast.get_node(id))
1830+
.find(|node| node.kind_name() == "integer")
1831+
.expect("integer exists");
1832+
assert_eq!(integer.byte_range(), 0..0);
1833+
}
1834+
1835+
/// `tree_spanning!` assigns the union of the captured node ranges only to the
1836+
/// template root.
1837+
#[test]
1838+
fn test_tree_spanning_assigns_union_to_root_only() {
1839+
let rule: Rule = rule!(
1840+
(call
1841+
method: (identifier) @name
1842+
receiver: (identifier) @recv
1843+
)
1844+
=>
1845+
call {
1846+
let arguments = tree_spanning!(
1847+
ctx,
1848+
[recv, name],
1849+
(argument_list argument: (integer "0"))
1850+
);
1851+
tree!((call method: {name} receiver: {recv} arguments: {arguments}))
1852+
}
1853+
);
1854+
1855+
let ast = run_and_ast("foo.bar()", vec![rule]);
1856+
let arguments = ast
1857+
.reachable_node_ids()
1858+
.into_iter()
1859+
.filter_map(|id| ast.get_node(id))
1860+
.find(|node| node.kind_name() == "argument_list")
1861+
.expect("argument list exists");
1862+
assert_eq!(arguments.byte_range(), 0..7);
1863+
1864+
let integer = ast
1865+
.reachable_node_ids()
1866+
.into_iter()
1867+
.filter_map(|id| ast.get_node(id))
1868+
.find(|node| node.kind_name() == "integer")
1869+
.expect("integer exists");
1870+
assert_eq!(integer.byte_range(), 0..0);
1871+
}
1872+
18021873
/// Explicit ranges on multiple results must not be widened to the range of the
18031874
/// input node matched by the rule.
18041875
#[test]

0 commit comments

Comments
 (0)