Skip to content

Commit 96abd2b

Browse files
committed
perf(parser): rewrite Walk to use TreeCursor (Task B1)
Replaces the recursive Node.Child(i) traversal with an iterative tree-sitter TreeCursor walk. Matches the canonical tree-sitter idiom and removes Go-level recursion frames per descent. Honest accounting: smacker's *TreeCursor.CurrentNode() still routes through Tree.cachedNode, so the per-visit *Node allocation is unchanged. The 91%-of-allocations cachedNode hot spot pprof flagged on airflow was driven by per-node *re-parse* (Task A1 fixed that by parsing once per file). Phase B's structural change keeps the public Walk(root, fn) API identical; callers and tests are untouched. Plan: docs/superpowers/plans/2026-05-13-enrich-oom-fix.md Task B1. Verification: - go test ./internal/parser/... ./internal/intelligence/extractor/... pass - go test ./... -count=1: 877 pass (unchanged from main) - Determinism preserved: pre-order DFS visitation order matches the recursive form exactly.
1 parent 9f54673 commit 96abd2b

1 file changed

Lines changed: 33 additions & 5 deletions

File tree

go/internal/parser/walk.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,43 @@ type Node = sitter.Node
1919
// recurse into the current node's children, false to skip them. Walking stops
2020
// when the visitor returns false at the root or when all descendants have
2121
// been visited. nil-safe.
22+
//
23+
// Implementation uses tree-sitter's TreeCursor for iterative traversal.
24+
// Compared to the previous recursive `n.Child(i)` form, the cursor avoids
25+
// Go-level recursion frames per descent and matches the canonical
26+
// tree-sitter walking idiom. Note: each visited node still flows through
27+
// smacker's per-Tree node cache (allocates one *Node on first visit per
28+
// node), so the allocation count is roughly the same as the recursive form
29+
// — the win is in stack discipline and code clarity, not GC pressure.
2230
func Walk(n *Node, visit func(*Node) bool) {
2331
if n == nil || visit == nil {
2432
return
2533
}
26-
if !visit(n) {
27-
return
28-
}
29-
for i := 0; i < int(n.ChildCount()); i++ {
30-
Walk(n.Child(i), visit)
34+
cur := sitter.NewTreeCursor(n)
35+
defer cur.Close()
36+
// Visit root.
37+
descend := visit(cur.CurrentNode())
38+
for {
39+
if descend && cur.GoToFirstChild() {
40+
descend = visit(cur.CurrentNode())
41+
continue
42+
}
43+
// No children to descend into; advance to next sibling, climbing
44+
// out of the subtree as necessary. The loop terminates when
45+
// GoToParent returns false (we have climbed back above the
46+
// original root).
47+
for {
48+
if cur.GoToNextSibling() {
49+
descend = visit(cur.CurrentNode())
50+
break
51+
}
52+
if !cur.GoToParent() {
53+
return
54+
}
55+
// After climbing to parent, the parent itself was already
56+
// visited when we descended into it — do NOT re-visit. Continue
57+
// trying GoToNextSibling at the parent's level.
58+
}
3159
}
3260
}
3361

0 commit comments

Comments
 (0)