Skip to content

Commit d70ddb8

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 d441910 commit d70ddb8

4 files changed

Lines changed: 161 additions & 0 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ impl<'a, C> BuildCtx<'a, C> {
157157
results
158158
}
159159

160+
/// Assign an explicit source range to a newly-built result root.
161+
#[doc(hidden)]
162+
pub fn set_node_source_range(&mut self, node: Id, source_range: Option<Range>) -> Id {
163+
if let Some(source_range) = source_range {
164+
self.ast.set_source_range(node, source_range);
165+
self.created_nodes.remove(&node);
166+
}
167+
node
168+
}
169+
160170
/// Look up a capture variable, returning its node Id.
161171
pub fn capture(&self, name: &str) -> Id {
162172
self.captures
@@ -180,6 +190,18 @@ impl<'a, C> BuildCtx<'a, C> {
180190
self.ast.source_text(id)
181191
}
182192

193+
/// Return the source range of a parsed or synthetic node.
194+
fn source_range_of(&self, id: Id) -> Option<Range> {
195+
self.ast.get_node(id).and_then(|node| node.source_range())
196+
}
197+
198+
/// Return an empty range between two non-overlapping nodes.
199+
pub fn empty_source_range_between(&self, left: Id, right: Id) -> Option<Range> {
200+
let left = self.source_range_of(left)?;
201+
let right = self.source_range_of(right)?;
202+
(left.end_byte <= right.start_byte).then(|| left.empty_at_end())
203+
}
204+
183205
/// Create a named AST node with the given kind and fields.
184206
pub fn node(&mut self, kind: &str, fields: Vec<(&str, Vec<Id>)>) -> Id {
185207
let kind_id = self
@@ -217,6 +239,12 @@ impl<'a, C> BuildCtx<'a, C> {
217239
self.create_named_token_with_range(kind, value.to_string(), source_range)
218240
}
219241

242+
/// Create a literal with an empty range at another node's start.
243+
pub fn literal_at_start_of(&mut self, kind: &'static str, value: &str, source: Id) -> Id {
244+
let source_range = self.source_range_of(source).map(Range::empty_at_start);
245+
self.literal_with_source_range(kind, value, source_range)
246+
}
247+
220248
/// Create a leaf node with an auto-generated unique name.
221249
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
222250
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)