diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 1a6e16068..2abdb7ad7 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -399,6 +399,21 @@ bool cbm_kind_in_set(TSNode node, const char **types) { return kind_in_set_strcmp(node, (const char *const *)types); } +/* Free the calling thread's node-type bitset cache (the calloc'd `bits` arrays + * that cbm_kind_in_set builds lazily). The cache is thread-local, so each worker + * thread and the main thread must call this at teardown (worker exit / process + * exit) for LeakSanitizer to report no leak. Safe if no cache was ever built. */ +void cbm_kind_in_set_free_cache(void) { + for (int i = 0; i < KS_SLOTS; i++) { + free(ks_cache[i].bits); + ks_cache[i].bits = NULL; + ks_cache[i].lang = NULL; + ks_cache[i].types = NULL; + ks_cache[i].nsyms = 0; + ks_cache[i].exact = false; + } +} + bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth) { TSNode cur = node; for (int i = 0; i < max_depth; i++) { diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index c1d158005..ea8154d4c 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -42,6 +42,10 @@ TSNode cbm_find_child_by_kind(TSNode parent, const char *kind); // Check if node kind matches a set of types (NULL-terminated array of strings). bool cbm_kind_in_set(TSNode node, const char **types); +// Free the calling thread's cbm_kind_in_set bitset cache (call at thread/process +// teardown so the thread-local cache is not reported as a leak). +void cbm_kind_in_set_free_cache(void); + // Check if node has an ancestor of the given kind, within max_depth levels. bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index daec7a661..4b5e0b884 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -48,6 +48,7 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 }; #include "pipeline/pipeline_internal.h" #include "pipeline/pass_lsp_cross.h" /* cbm_pxc_* helpers for fused cross-file LSP */ #include "pipeline/lsp_resolve.h" +#include "helpers.h" /* cbm_kind_in_set_free_cache — per-worker-thread cache teardown */ #include "pipeline/worker_pool.h" #include "foundation/compat.h" #include "foundation/compat_thread.h" @@ -635,6 +636,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) { /* Final cleanup (parser already destroyed in loop, just slab state) */ cbm_slab_destroy_thread(); + cbm_kind_in_set_free_cache(); /* free this worker thread's node-type bitset cache */ } static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_entries, diff --git a/tests/test_main.c b/tests/test_main.c index a30964b49..51c0b03cc 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -80,6 +80,11 @@ extern void suite_incremental(void); extern void suite_simhash(void); extern void suite_stack_overflow(void); +/* Free the main thread's thread-local node-type bitset cache before exit so + * LeakSanitizer (Linux x64) doesn't report it. Worker threads free their own + * caches at thread teardown (pass_parallel.c). */ +extern void cbm_kind_in_set_free_cache(void); + int main(void) { printf("\n codebase-memory-mcp C test suite\n"); @@ -207,7 +212,8 @@ int main(void) { RUN_SUITE(incremental); - /* Release sqlite3 internal caches so ASan doesn't report them as leaks */ + /* Release process-lifetime caches so LeakSanitizer reports no leaks. */ + cbm_kind_in_set_free_cache(); sqlite3_shutdown(); TEST_SUMMARY(); }