Problem
Explorer.call_graph is built once (ensureCallGraph, guarded by call_graph != null) and never invalidated: the file-edit commit path refreshes deps, the symbol index, and line offsets, but not call_graph/call_centrality. Every codedb_callpath (and the graph-distance rerank boost feeding ranked search) answers from the process-start snapshot of the graph.
It's worse than staleness: CallGraph.node_path/node_name are borrowed slices into outline symbol memory (CallGraph.deinit frees only the arrays, not the strings). Re-indexing a file frees those outlines, so the graph's name/path tables dangle — reads through them after an edit are use-after-free. Under the testing allocator this shows up as silently-wrong results (freed memory no longer matches any query name); under other allocators it can return corrupt paths.
Found while perf-auditing the graph queries (the same audit that produced #651): the invalidation gap was visible statically, and the failing test then exposed the dangling-slice aspect (an earlier test variant "passed" only because the poisoned freed memory matched nothing).
Failing Test
src/test_explore.zig (branch issue-656-failing-test), red on current release/0.2.5828:
test "issue-656: call graph is stale and dangling after a file edit" {
var explorer_inst = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
defer explorer_inst.deinit();
try explorer_inst.indexFile("calls.zig",
\\pub fn alpha() void {}
\\pub fn beta() void {}
);
// First query builds the call graph: no alpha -> beta edge yet.
const before = try explorer_inst.findCallPath("alpha", "beta", testing.allocator, 4);
try testing.expect(before == null);
// Edit the file so alpha NOW calls beta.
try explorer_inst.indexFile("calls.zig",
\\pub fn alpha() void {
\\ beta();
\\}
\\pub fn beta() void {}
);
const after = try explorer_inst.findCallPath("alpha", "beta", testing.allocator, 4);
defer if (after) |steps| testing.allocator.free(steps);
try testing.expect(after != null); // fails: stale/dangling graph can't see the new edge
}
Expected
After a file is re-indexed, codedb_callpath reflects the edited call sites (and never reads freed memory).
Fix
Two-part, smallest-safe shape:
- Invalidate on commit: in the file-commit path, deinit + null
call_graph (and call_centrality if its snapshot-restored copy should track edits) and bumpSearchGen() — next graph query lazily rebuilds, matching ensureSymbolIndex's pattern. Rebuild-per-edit is acceptable: build cost is the existing first-query cost, and edits are much rarer than queries.
- Own the strings (or dupe node_path/node_name at build time) so a not-yet-invalidated graph can never dangle even if a future code path misses the invalidation hook.
Problem
Explorer.call_graphis built once (ensureCallGraph, guarded bycall_graph != null) and never invalidated: the file-edit commit path refreshes deps, the symbol index, and line offsets, but notcall_graph/call_centrality. Everycodedb_callpath(and the graph-distance rerank boost feeding ranked search) answers from the process-start snapshot of the graph.It's worse than staleness:
CallGraph.node_path/node_nameare borrowed slices into outline symbol memory (CallGraph.deinitfrees only the arrays, not the strings). Re-indexing a file frees those outlines, so the graph's name/path tables dangle — reads through them after an edit are use-after-free. Under the testing allocator this shows up as silently-wrong results (freed memory no longer matches any query name); under other allocators it can return corrupt paths.Found while perf-auditing the graph queries (the same audit that produced #651): the invalidation gap was visible statically, and the failing test then exposed the dangling-slice aspect (an earlier test variant "passed" only because the poisoned freed memory matched nothing).
Failing Test
src/test_explore.zig(branchissue-656-failing-test), red on currentrelease/0.2.5828:Expected
After a file is re-indexed,
codedb_callpathreflects the edited call sites (and never reads freed memory).Fix
Two-part, smallest-safe shape:
call_graph(andcall_centralityif its snapshot-restored copy should track edits) andbumpSearchGen()— next graph query lazily rebuilds, matchingensureSymbolIndex's pattern. Rebuild-per-edit is acceptable: build cost is the existing first-query cost, and edits are much rarer than queries.