Skip to main content

lanekeep_nodes/
nodes.rs

1//! Node handles: how a parsed file's nodes cross into a rule.
2//!
3//! Architecture §14 requires nodes to cross the boundary as opaque handles rather than as
4//! materialized objects. Materializing an AST is the cost that makes native tooling with
5//! JavaScript plugins slow, and it is a decision that cannot be walked back once rules
6//! depend on the object shape. Nothing about that argument is specific to one engine, which
7//! is why this type lives in its own crate rather than inside the engine that first needed
8//! it — see the crate-level docs for which engines that is.
9//!
10//! # Why paths rather than stored nodes
11//!
12//! The obvious arena holds `Node<'tree>` and hands out indices. It does not compile against
13//! `lanekeep-js`'s engine: rquickjs's `Function::new` requires `'static` closures, so
14//! nothing captured by a host function may borrow from a tree living on the caller's stack.
15//!
16//! So the arena **owns** the tree and stores, for each handle, the path of child indices
17//! from the root. Resolving a handle walks that path — `O(depth)`, where depth is typically
18//! ten to thirty — and every method does its work internally rather than returning a
19//! borrowed `Node`, which is what keeps the borrow checker satisfied without `unsafe`.
20//!
21//! Two properties this buys, both of which matter more than the lookup cost, and neither of
22//! which is specific to the engine that first needed them:
23//!
24//! **Laziness.** Only nodes actually handed to a rule are interned. A file whose query
25//! matches nothing costs nothing here, which is the whole point of the query gate.
26//!
27//! **Stable identity.** Handles are interned by node id, so the same node reached twice —
28//! a parent lookup from two siblings, say — yields the same number. A rule comparing two
29//! handles for equality relies on that, and it has to mean what it appears to mean.
30
31use lanekeep_query::CompiledQuery;
32use std::collections::HashMap;
33
34use lanekeep_lang::binding::{Binding, BindingResolver};
35use tree_sitter::{Node, Tree};
36
37/// An opaque reference to a node, as seen from rule code.
38pub type Handle = u32;
39
40/// Child indices of a node, as `u32`.
41///
42/// tree-sitter reports `child_count` as `usize` but takes `u32` in `child`, so the
43/// conversion lives here once rather than at each of the three call sites.
44fn child_indices(node: Node<'_>) -> std::ops::Range<u32> {
45    0..u32::try_from(node.child_count()).unwrap_or(u32::MAX)
46}
47
48/// Owns a parsed tree and the handles issued against it.
49#[derive(Debug)]
50pub struct NodeArena {
51    tree: Tree,
52    source: String,
53    /// Handle to the path of child indices from the root. The root's path is empty.
54    paths: Vec<Vec<u32>>,
55    /// Node id to handle, so a node reached twice gets the same handle both times.
56    by_id: HashMap<usize, Handle>,
57}
58
59impl NodeArena {
60    /// Take ownership of a tree and its source.
61    ///
62    /// The root is interned as handle `0`, so rule code always has somewhere to start.
63    #[must_use]
64    pub fn new(tree: Tree, source: String) -> Self {
65        let root_id = tree.root_node().id();
66        let mut arena = Self {
67            tree,
68            source,
69            paths: Vec::new(),
70            by_id: HashMap::new(),
71        };
72        arena.paths.push(Vec::new());
73        arena.by_id.insert(root_id, 0);
74        arena
75    }
76
77    /// The root node's handle.
78    ///
79    /// A constant rather than a method: the root is interned first, so it is always zero.
80    pub const ROOT: Handle = 0;
81
82    /// The source this tree was parsed from.
83    #[must_use]
84    pub fn source(&self) -> &str {
85        &self.source
86    }
87
88    /// How many handles have been issued. Interning is lazy, so this reflects how much of
89    /// the tree a rule actually touched.
90    #[must_use]
91    pub fn len(&self) -> usize {
92        self.paths.len()
93    }
94
95    /// Whether only the root has been interned.
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.paths.len() <= 1
99    }
100
101    /// Walk a path from the root.
102    fn node_at(&self, path: &[u32]) -> Option<Node<'_>> {
103        let mut node = self.tree.root_node();
104        for index in path {
105            node = node.child(*index)?;
106        }
107        Some(node)
108    }
109
110    /// Resolve a handle to its node.
111    fn node(&self, handle: Handle) -> Option<Node<'_>> {
112        let path = self.paths.get(handle as usize)?;
113        self.node_at(path)
114    }
115
116    /// Issue a handle for a path, reusing the existing one when the node is already known.
117    fn intern(&mut self, id: usize, path: Vec<u32>) -> Handle {
118        if let Some(existing) = self.by_id.get(&id) {
119            return *existing;
120        }
121        // A tree large enough to overflow this would have exhausted memory long before.
122        let handle = Handle::try_from(self.paths.len()).unwrap_or(Handle::MAX);
123        self.paths.push(path);
124        self.by_id.insert(id, handle);
125        handle
126    }
127
128    /// Intern a node reached by extending a known path.
129    ///
130    /// Split into "read the node's id, then intern" so the immutable borrow of `self` ends
131    /// before the mutable one begins.
132    fn intern_child(&mut self, parent_path: &[u32], index: u32) -> Option<Handle> {
133        let mut path = parent_path.to_vec();
134        path.push(index);
135        let id = self.node_at(&path)?.id();
136        Some(self.intern(id, path))
137    }
138
139    /// The node's kind, as the grammar names it.
140    #[must_use]
141    pub fn kind(&self, handle: Handle) -> Option<&'static str> {
142        self.node(handle).map(|node| node.kind())
143    }
144
145    /// Whether the node is named, as opposed to an anonymous token such as a bracket.
146    #[must_use]
147    pub fn is_named(&self, handle: Handle) -> Option<bool> {
148        self.node(handle).map(|node| node.is_named())
149    }
150
151    /// The source text the node spans.
152    #[must_use]
153    pub fn text(&self, handle: Handle) -> Option<&str> {
154        let node = self.node(handle)?;
155        self.source.get(node.byte_range())
156    }
157
158    /// One-based line and column of the node's start.
159    #[must_use]
160    pub fn position(&self, handle: Handle) -> Option<(u32, u32)> {
161        let node = self.node(handle)?;
162        let start = node.start_position();
163        Some((
164            u32::try_from(start.row)
165                .unwrap_or(u32::MAX)
166                .saturating_add(1),
167            u32::try_from(start.column)
168                .unwrap_or(u32::MAX)
169                .saturating_add(1),
170        ))
171    }
172
173    /// The node's byte range in the source.
174    #[must_use]
175    pub fn byte_range(&self, handle: Handle) -> Option<(usize, usize)> {
176        self.node(handle)
177            .map(|node| (node.start_byte(), node.end_byte()))
178    }
179
180    /// What the identifier at a handle refers to.
181    ///
182    /// Takes the resolver rather than holding one so the arena stays language-agnostic,
183    /// and does the work internally so the borrowed `Node` never escapes.
184    #[must_use]
185    pub fn resolve_binding(
186        &self,
187        handle: Handle,
188        resolver: &dyn BindingResolver,
189    ) -> Option<Binding> {
190        let node = self.node(handle)?;
191        resolver.resolve(&self.tree, &self.source, node)
192    }
193
194    /// Whether the identifier at a handle shadows an outer binding of the same name.
195    #[must_use]
196    pub fn is_shadowed(&self, handle: Handle, resolver: &dyn BindingResolver) -> bool {
197        self.node(handle)
198            .is_some_and(|node| resolver.is_shadowed(&self.tree, &self.source, node))
199    }
200
201    /// The tree, for running queries against.
202    ///
203    /// Nodes obtained this way borrow the arena immutably, so they cannot be interned
204    /// while still held. Use [`NodeArena::path_of`] to reduce them to paths first, drop
205    /// them, then call [`NodeArena::intern_path`]. The two-phase shape is not incidental:
206    /// it is what lets the arena own the tree, which is what makes the handles `'static`
207    /// enough for the engine to hold.
208    #[must_use]
209    pub const fn tree(&self) -> &Tree {
210        &self.tree
211    }
212
213    /// Reduce a node to a path, so it can be interned after its borrow ends.
214    ///
215    /// Returns `None` for a node belonging to a different tree. Interning one under a path
216    /// that happened to resolve locally would hand a rule a handle pointing at an
217    /// unrelated node — wrong results, no error.
218    #[must_use]
219    pub fn path_of(&self, node: Node<'_>) -> Option<Vec<u32>> {
220        // Walk up to the root, recording each child index.
221        let mut path = Vec::new();
222        let mut current = node;
223        while let Some(parent) = current.parent() {
224            let index = child_indices(parent)
225                .find(|i| parent.child(*i).is_some_and(|c| c.id() == current.id()))?;
226            path.push(index);
227            current = parent;
228        }
229        path.reverse();
230
231        if self
232            .node_at(&path)
233            .is_none_or(|found| found.id() != node.id())
234        {
235            return None;
236        }
237        Some(path)
238    }
239
240    /// Issue a handle for a path produced by [`NodeArena::path_of`].
241    pub fn intern_path(&mut self, path: Vec<u32>) -> Option<Handle> {
242        let id = self.node_at(&path)?.id();
243        Some(self.intern(id, path))
244    }
245
246    /// The node's parent, if it is not the root.
247    pub fn parent(&mut self, handle: Handle) -> Option<Handle> {
248        let path = self.paths.get(handle as usize)?.clone();
249        if path.is_empty() {
250            return None;
251        }
252
253        let parent_path = path[..path.len() - 1].to_vec();
254        let id = self.node_at(&parent_path)?.id();
255        Some(self.intern(id, parent_path))
256    }
257
258    /// Every child, including anonymous tokens.
259    pub fn children(&mut self, handle: Handle) -> Vec<Handle> {
260        self.children_matching(handle, false)
261    }
262
263    /// Named children only, which is what a rule almost always wants.
264    pub fn named_children(&mut self, handle: Handle) -> Vec<Handle> {
265        self.children_matching(handle, true)
266    }
267
268    fn children_matching(&mut self, handle: Handle, named_only: bool) -> Vec<Handle> {
269        let Some(path) = self.paths.get(handle as usize).cloned() else {
270            return Vec::new();
271        };
272
273        let indices: Vec<u32> = {
274            let Some(node) = self.node_at(&path) else {
275                return Vec::new();
276            };
277            child_indices(node)
278                .filter(|i| !named_only || node.child(*i).is_some_and(|c| c.is_named()))
279                .collect()
280        };
281
282        indices
283            .into_iter()
284            .filter_map(|i| self.intern_child(&path, i))
285            .collect()
286    }
287
288    /// Matches of a query, scoped to one node's subtree.
289    ///
290    /// Two-phase like everything else here: capture paths are collected while the tree is
291    /// borrowed, then interned once that borrow has ended. The arena owns the tree, so a
292    /// handle cannot be minted while a `Node` derived from it is alive.
293    #[must_use]
294    pub fn query_subtree(
295        &self,
296        handle: Handle,
297        query: &CompiledQuery,
298    ) -> Vec<Vec<(String, Vec<u32>)>> {
299        let Some(path) = self.paths.get(handle as usize) else {
300            return Vec::new();
301        };
302        let Some(node) = self.node_at(path) else {
303            return Vec::new();
304        };
305
306        let mut found = Vec::new();
307        query.for_each_match_in(node, self.source.as_bytes(), |m| {
308            found.push(
309                m.captures
310                    .iter()
311                    .filter_map(|(name, node)| {
312                        self.path_of(*node).map(|path| ((*name).to_owned(), path))
313                    })
314                    .collect::<Vec<_>>(),
315            );
316        });
317        found
318    }
319
320    /// The nearest ancestor a query matches at, with that match's captures.
321    ///
322    /// "Matches at" rather than "matches within": the query runs rooted at each ancestor in
323    /// turn, and a match counts only if it captured that ancestor. Without that, a query
324    /// matching anything anywhere inside would make the outermost ancestor the answer every
325    /// time, which is never what a rule walking upward wants.
326    #[must_use]
327    pub fn closest_ancestor_paths(
328        &self,
329        handle: Handle,
330        query: &CompiledQuery,
331    ) -> Option<Vec<(String, Vec<u32>)>> {
332        let path = self.paths.get(handle as usize)?.clone();
333
334        // Innermost first, so the closest ancestor wins.
335        for depth in (0..path.len()).rev() {
336            let Some(ancestor) = self.node_at(&path[..depth]) else {
337                continue;
338            };
339
340            let mut matched: Option<Vec<(String, Vec<u32>)>> = None;
341            query.for_each_match_in(ancestor, self.source.as_bytes(), |m| {
342                if matched.is_some() || !m.captures.iter().any(|(_, node)| *node == ancestor) {
343                    return;
344                }
345                matched = Some(
346                    m.captures
347                        .iter()
348                        .filter_map(|(name, node)| {
349                            self.path_of(*node).map(|p| ((*name).to_owned(), p))
350                        })
351                        .collect(),
352                );
353            });
354
355            if matched.is_some() {
356                return matched;
357            }
358        }
359        None
360    }
361
362    /// Ancestors, innermost first, ending at the root.
363    pub fn ancestors(&mut self, handle: Handle) -> Vec<Handle> {
364        let Some(path) = self.paths.get(handle as usize).cloned() else {
365            return Vec::new();
366        };
367
368        let mut out = Vec::with_capacity(path.len());
369        for depth in (0..path.len()).rev() {
370            let ancestor_path = path[..depth].to_vec();
371            let Some(id) = self.node_at(&ancestor_path).map(|n| n.id()) else {
372                break;
373            };
374            out.push(self.intern(id, ancestor_path));
375        }
376        out
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use lanekeep_lang::Language;
383    use lanekeep_lang_js::TypeScript;
384
385    use super::*;
386
387    fn arena(source: &str) -> NodeArena {
388        let mut parser = tree_sitter::Parser::new();
389        parser
390            .set_language(&TypeScript.grammar())
391            .expect("grammar loads");
392        let tree = parser.parse(source, None).expect("parses");
393        NodeArena::new(tree, source.to_owned())
394    }
395
396    #[test]
397    fn the_root_is_always_handle_zero() {
398        let arena = arena("const x = 1;");
399        assert_eq!(NodeArena::ROOT, 0);
400        assert_eq!(arena.kind(0), Some("program"));
401    }
402
403    #[test]
404    fn resolves_kind_text_and_position() {
405        let mut arena = arena("const x = 1;\nconst y = 2;");
406        let statements = arena.named_children(NodeArena::ROOT);
407        assert_eq!(statements.len(), 2);
408
409        assert_eq!(arena.kind(statements[0]), Some("lexical_declaration"));
410        assert_eq!(arena.text(statements[0]), Some("const x = 1;"));
411        assert_eq!(arena.position(statements[0]), Some((1, 1)));
412        assert_eq!(arena.position(statements[1]), Some((2, 1)));
413    }
414
415    #[test]
416    fn walks_down_and_back_up() {
417        let mut arena = arena("const x = 1;");
418        let root = NodeArena::ROOT;
419        let declaration = arena.named_children(root)[0];
420        let declarator = arena.named_children(declaration)[0];
421
422        assert_eq!(arena.parent(declarator), Some(declaration));
423        assert_eq!(arena.parent(declaration), Some(root));
424        assert_eq!(arena.parent(root), None, "the root has no parent");
425    }
426
427    #[test]
428    fn handles_are_stable_for_the_same_node() {
429        // Rules compare handles with `===`, so reaching one node by two routes has to
430        // produce the same number. Without interning it would produce two, and a rule
431        // checking whether two captures are the same node would silently always say no.
432        let mut arena = arena("const x = 1;");
433        let root = NodeArena::ROOT;
434        let declaration = arena.named_children(root)[0];
435
436        let again = arena.named_children(root)[0];
437        assert_eq!(
438            declaration, again,
439            "the same child must intern to the same handle"
440        );
441
442        let declarator = arena.named_children(declaration)[0];
443        assert_eq!(
444            arena.parent(declarator),
445            Some(declaration),
446            "reaching a node from below must give the handle it already had"
447        );
448    }
449
450    #[test]
451    fn interning_is_lazy() {
452        // The property that makes this cheap: a file whose query matched nothing must not
453        // have paid to materialize its tree.
454        let arena = arena("const a = 1; const b = 2; function c() { return [1,2,3] }");
455        assert!(
456            arena.is_empty(),
457            "only the root should be interned before any traversal"
458        );
459        assert_eq!(arena.len(), 1);
460    }
461
462    #[test]
463    fn only_touched_nodes_are_interned() {
464        let mut arena = arena("const a = 1; const b = 2; const c = 3;");
465        let before = arena.len();
466        let _ = arena.named_children(NodeArena::ROOT);
467        let after = arena.len();
468
469        assert!(after > before);
470        assert!(
471            after < 20,
472            "should intern three statements, not the whole tree: {after}"
473        );
474    }
475
476    #[test]
477    fn named_children_excludes_anonymous_tokens() {
478        let mut arena = arena("const x = 1;");
479        let declaration = arena.named_children(NodeArena::ROOT)[0];
480
481        let all = arena.children(declaration);
482        let named = arena.named_children(declaration);
483        assert!(all.len() > named.len(), "`const` and `;` are anonymous");
484        assert!(named.iter().all(|h| arena.is_named(*h) == Some(true)));
485    }
486
487    #[test]
488    fn ancestors_run_innermost_first_and_end_at_the_root() {
489        let mut arena = arena("function f() { return 1; }");
490        let root = NodeArena::ROOT;
491        let function = arena.named_children(root)[0];
492        let body = arena
493            .named_children(function)
494            .last()
495            .copied()
496            .expect("has a body");
497        let statement = arena.named_children(body)[0];
498
499        let ancestors = arena.ancestors(statement);
500        assert_eq!(ancestors.first(), Some(&body), "innermost first");
501        assert_eq!(ancestors.last(), Some(&root), "ending at the root");
502        assert!(ancestors.contains(&function));
503    }
504
505    #[test]
506    fn the_root_has_no_ancestors() {
507        let mut arena = arena("const x = 1;");
508        assert!(arena.ancestors(NodeArena::ROOT).is_empty());
509    }
510
511    #[test]
512    fn an_unknown_handle_yields_nothing_rather_than_panicking() {
513        // Rule code is arbitrary and may pass any number at all.
514        let mut arena = arena("const x = 1;");
515        assert_eq!(arena.kind(9999), None);
516        assert_eq!(arena.text(9999), None);
517        assert_eq!(arena.position(9999), None);
518        assert_eq!(arena.parent(9999), None);
519        assert!(arena.children(9999).is_empty());
520        assert!(arena.ancestors(9999).is_empty());
521    }
522
523    #[test]
524    fn interns_a_node_reached_through_the_tree() {
525        // The shape real callers use: find nodes against `tree()`, reduce them to paths
526        // while the borrow is live, then intern once it has ended.
527        let mut arena = arena("const x = 1;");
528
529        let (path, expected_kind) = {
530            let target = arena
531                .tree()
532                .root_node()
533                .child(0)
534                .and_then(|n| n.child(1))
535                .expect("has a declarator");
536            (arena.path_of(target).expect("has a path"), target.kind())
537        };
538
539        let handle = arena.intern_path(path.clone()).expect("interns");
540        assert_eq!(arena.kind(handle), Some(expected_kind));
541        assert_eq!(
542            arena.intern_path(path),
543            Some(handle),
544            "interning the same path twice must give the same handle"
545        );
546    }
547
548    #[test]
549    fn rejects_a_node_from_a_different_tree() {
550        // Reducing a foreign node to a path that happens to resolve locally would hand a
551        // rule a handle pointing at an unrelated node — wrong results with no error.
552        let mut parser = tree_sitter::Parser::new();
553        parser
554            .set_language(&TypeScript.grammar())
555            .expect("grammar loads");
556        let other = parser
557            .parse("function totallyDifferent() { return 42 }", None)
558            .expect("parses");
559        let foreign = other.root_node().child(0).expect("has a child");
560
561        let arena = arena("const x = 1;");
562        assert_eq!(
563            arena.path_of(foreign),
564            None,
565            "a node from another tree must not be reducible to a path here"
566        );
567    }
568
569    #[test]
570    fn text_is_correct_for_multibyte_source() {
571        let mut arena = arena("const emoji = '🎯';\nconst after = 1;");
572        let statements = arena.named_children(NodeArena::ROOT);
573        assert_eq!(arena.text(statements[0]), Some("const emoji = '🎯';"));
574        assert_eq!(
575            arena.position(statements[1]),
576            Some((2, 1)),
577            "a multibyte character must not shift the following line"
578        );
579    }
580}