Problem
rebuildSymbolIndexFor keys the global symbol_index with sym.name slices owned by the file's outline (src/explore.zig:5081). When that file is re-indexed, commitParsedFileOwnedOutline deinits the prior outline (src/explore.zig:915), freeing the very bytes the map hashes and compares — but the map entry survives whenever any other file shares the symbol name, because removeSymbolIndexFor only drops entries whose location list becomes empty. In Zig code, init/deinit/main are shared by nearly every file, so the first edit of whichever file first inserted a shared name leaves a dangling key. The same happens on file deletion: Explorer.removeFile frees the outline (src/explore.zig:1513) after removeSymbolIndexFor kept shared-name entries alive.
Every subsequent lookup or iteration eql()s against freed memory — UB in release builds (with reused memory, potentially garbage names in symbolSearch results); under the DebugAllocator poison the entry becomes unreachable, silently degrading the O(1) index to the outline safety scans for the daemon's lifetime.
Failing Test
test_explore.zig — fails on current release tip (error.SymbolLostFromIndex):
test "issue-586: symbol_index keys must survive re-index of the file that first inserted them" {
var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
defer explorer.deinit();
// a.zig inserts sharedFn first -> the map key aliases a.zig's outline string.
try explorer.indexFile("src/a.zig", "pub fn sharedFn() void {}\n");
try explorer.indexFile("src/b.zig", "pub fn sharedFn() void {}\n");
// Re-index the key owner: its old outline is freed; the entry stays alive
// because b.zig still holds a location.
try explorer.indexFile("src/a.zig", "pub fn sharedFn() void {}\npub fn other() void {}\n");
const locs = explorer.symbol_index.get("sharedFn") orelse return error.SymbolLostFromIndex;
try testing.expect(locs.items.len >= 2);
}
Expected
symbol_index entries stay valid and reachable regardless of which file's outline is replaced or removed.
Fix
Make the map own its keys: dupe on first insert in rebuildSymbolIndexFor, free the key when removeSymbolIndexFor drops an emptied entry, and free keys in Explorer.deinit. One dupe per unique symbol name; uniform ownership also covers the snapshot fast-load case where names are borrowed from the mmap'd outline section.
Problem
rebuildSymbolIndexForkeys the globalsymbol_indexwithsym.nameslices owned by the file's outline (src/explore.zig:5081). When that file is re-indexed,commitParsedFileOwnedOutlinedeinits the prior outline (src/explore.zig:915), freeing the very bytes the map hashes and compares — but the map entry survives whenever any other file shares the symbol name, becauseremoveSymbolIndexForonly drops entries whose location list becomes empty. In Zig code,init/deinit/mainare shared by nearly every file, so the first edit of whichever file first inserted a shared name leaves a dangling key. The same happens on file deletion:Explorer.removeFilefrees the outline (src/explore.zig:1513) afterremoveSymbolIndexForkept shared-name entries alive.Every subsequent lookup or iteration
eql()s against freed memory — UB in release builds (with reused memory, potentially garbage names insymbolSearchresults); under the DebugAllocator poison the entry becomes unreachable, silently degrading the O(1) index to the outline safety scans for the daemon's lifetime.Failing Test
test_explore.zig— fails on current release tip (error.SymbolLostFromIndex):Expected
symbol_indexentries stay valid and reachable regardless of which file's outline is replaced or removed.Fix
Make the map own its keys: dupe on first insert in
rebuildSymbolIndexFor, free the key whenremoveSymbolIndexFordrops an emptied entry, and free keys inExplorer.deinit. One dupe per unique symbol name; uniform ownership also covers the snapshot fast-load case where names are borrowed from the mmap'd outline section.