Skip to main content

lanekeep_js/
nodes.rs

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