diff --git a/src/store/store.c b/src/store/store.c index 92b78a2ca..069e06b1d 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -4123,10 +4123,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge bind_text(stmt, SKIP_ONE, project); sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND); + /* Both arrays are STORE-class memory-core blocks: the caller releases them + * with cbm_store_free_call_edges, the only free that matches. */ int cap = ST_INIT_CAP_16; int n = 0; - int64_t *src = malloc((size_t)cap * sizeof(int64_t)); - int64_t *tgt = malloc((size_t)cap * sizeof(int64_t)); + int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t)); + int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t)); + if (!src || !tgt) { + sqlite3_finalize(stmt); + cbm_store_free_call_edges(src, tgt); + store_set_error(s, "fetch_call_edges: out of memory"); + return CBM_STORE_ERR; + } int scan_rc; while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) { if (max_edges > 0 && n >= max_edges) { @@ -4136,9 +4144,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge break; } if (n >= cap) { + size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t); + int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes); + if (grown_src) { + src = grown_src; + } + int64_t *grown_tgt = + grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL; + if (grown_tgt) { + tgt = grown_tgt; + } + if (!grown_src || !grown_tgt) { + sqlite3_finalize(stmt); + cbm_store_free_call_edges(src, tgt); + store_set_error(s, "fetch_call_edges: out of memory"); + return CBM_STORE_ERR; + } cap *= ST_GROWTH; - src = safe_realloc(src, (size_t)cap * sizeof(int64_t)); - tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t)); } src[n] = sqlite3_column_int64(stmt, 0); tgt[n] = sqlite3_column_int64(stmt, 1); @@ -4146,8 +4168,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge } sqlite3_finalize(stmt); if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) { - free(src); - free(tgt); + cbm_store_free_call_edges(src, tgt); store_set_error_sqlite(s, "fetch_call_edges scan"); return CBM_STORE_ERR; } @@ -4158,8 +4179,8 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge } void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) { - free(src); - free(tgt); + cbm_free(CBM_MEM_CLASS_STORE, src); + cbm_free(CBM_MEM_CLASS_STORE, tgt); } int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, @@ -10499,7 +10520,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * } void cbm_leiden_free(cbm_louvain_result_t *result) { - free(result); + cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */ } /* ── Architecture: community clusters via Leiden ───────────────── */ diff --git a/src/store/store.h b/src/store/store.h index 3e2f3a546..59c281443 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -708,7 +708,8 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e); int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count); /* Fetch all CALLS edges among Function/Method nodes for a project as parallel - * (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis. + * (source_id, target_id) arrays, released together with + * cbm_store_free_call_edges. For SCC / cycle analysis. * Stops at max_edges and sets *truncated — never a silent cap. Returns * CBM_STORE_OK (or _ERR); *count is the number returned. */ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges, @@ -1291,7 +1292,7 @@ typedef struct { * reported community is internally connected. The resolution parameter * controls granularity (higher -> more, smaller communities); 1.0 is standard. * Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER; - * the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */ + * the caller releases it with cbm_leiden_free(*out). */ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count); diff --git a/src/ui/http_server.c b/src/ui/http_server.c index ffc834f6d..6e153686e 100644 --- a/src/ui/http_server.c +++ b/src/ui/http_server.c @@ -2042,7 +2042,7 @@ static bool attach_missed_graph(yyjson_mut_doc *mdoc, yyjson_mut_val *mroot, cbm return false; } yyjson_doc *mldoc = yyjson_read(mjson, strlen(mjson), 0); - free(mjson); + cbm_layout_free_json(mjson); if (!mldoc) { return false; } @@ -2164,7 +2164,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { return; } cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json); - free(scoped_json); + cbm_layout_free_json(scoped_json); return; } @@ -2200,13 +2200,13 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { if (linked_count == 0 && missed_graph) { cbm_store_close(store); cbm_http_replyf(c, 200, g_cors_json, "%s", primary_json); - free(primary_json); + cbm_layout_free_json(primary_json); return; } /* Parse primary JSON and append missed_graph + linked_projects */ yyjson_doc *pdoc = yyjson_read(primary_json, strlen(primary_json), 0); - free(primary_json); + cbm_layout_free_json(primary_json); if (!pdoc) { cbm_store_close(store); cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON parse failed\"}"); @@ -2258,7 +2258,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { /* Parse linked project layout */ yyjson_doc *lpdoc = yyjson_read(lp_json, strlen(lp_json), 0); - free(lp_json); + cbm_layout_free_json(lp_json); if (!lpdoc) { cbm_store_close(lp_store); free(linked[li]); diff --git a/src/ui/layout3d.c b/src/ui/layout3d.c index 89497837a..c9abaf773 100644 --- a/src/ui/layout3d.c +++ b/src/ui/layout3d.c @@ -14,6 +14,7 @@ #include "ui/layout3d.h" #include "ui/layout_internal.h" #include "foundation/log.h" +#include "foundation/json_writer.h" #include "foundation/mem_core.h" #include "foundation/platform.h" @@ -878,10 +879,12 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) { yyjson_mut_obj_add_val(doc, root, "edges", ea); yyjson_mut_obj_add_int(doc, root, "total_nodes", r->total_nodes); + /* The output string is an OTHER-class memory-core block, released by + * cbm_layout_free_json; the document keeps its own allocator. */ size_t len = 0; yyjson_write_err write_err = {0}; - char *json = - yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, NULL, &len, &write_err); + char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, + cbm_json_writer_allocator(), &len, &write_err); yyjson_mut_doc_free(doc); if (!json) { char code[CBM_SZ_32]; @@ -893,5 +896,5 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) { } void cbm_layout_free_json(char *json) { - free(json); + cbm_free(CBM_MEM_CLASS_OTHER, json); } diff --git a/src/ui/layout3d.h b/src/ui/layout3d.h index 29b13712a..6d4cd22ed 100644 --- a/src/ui/layout3d.h +++ b/src/ui/layout3d.h @@ -69,7 +69,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project, /* Release a computed layout result and its fields through their matching allocators. */ void cbm_layout_free(cbm_layout_result_t *result); -/* Serialize layout result to JSON string. Caller must free(). */ +/* Serialize layout result to JSON string. Release with cbm_layout_free_json(). */ char *cbm_layout_to_json(const cbm_layout_result_t *result); /* Free the JSON string returned by cbm_layout_to_json. */ diff --git a/tests/test_store_arch.c b/tests/test_store_arch.c index 50d434563..92af0994e 100644 --- a/tests/test_store_arch.c +++ b/tests/test_store_arch.c @@ -1431,6 +1431,64 @@ TEST(fetch_call_edges_empty_project) { PASS(); } +/* The two arrays cbm_store_fetch_call_edges hands out are memory-core blocks of + * the STORE class, and cbm_store_free_call_edges is what takes them back. Both + * halves are measured: the fetch adds exactly the two arrays and the paired free + * removes exactly those, so a raw allocation on either side fails. Thirty-six + * edges outgrow the initial capacity of 16, so the growth path is covered too. */ +TEST(fetch_call_edges_memory_core_ownership) { + enum { FN = 37 }; + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + cbm_store_upsert_project(s, "grow", "/tmp/grow"); + int64_t ids[FN]; + for (int i = 0; i < FN; i++) { + char name[32]; + char qn[64]; + snprintf(name, sizeof(name), "f%d", i); + snprintf(qn, sizeof(qn), "grow.pkg.f%d", i); + cbm_node_t n = {.project = "grow", + .label = "Function", + .name = name, + .qualified_name = qn, + .file_path = "pkg/f.go"}; + ids[i] = cbm_store_upsert_node(s, &n); + ASSERT_TRUE(ids[i] > 0); + } + for (int i = 0; i + 1 < FN; i++) { + cbm_edge_t e = { + .project = "grow", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"}; + ASSERT_TRUE(cbm_store_insert_edge(s, &e) > 0); + } + + size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE); + size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE); + int64_t *src = NULL; + int64_t *tgt = NULL; + int count = 0; + bool truncated = true; + ASSERT_EQ(cbm_store_fetch_call_edges(s, "grow", 100, &src, &tgt, &count, &truncated), + CBM_STORE_OK); + ASSERT_EQ(count, FN - 1); + ASSERT_FALSE(truncated); + ASSERT_NOT_NULL(src); + ASSERT_NOT_NULL(tgt); + /* Ordered by (source_id, target_id): growth kept every pair in place. */ + ASSERT_EQ(src[0], ids[0]); + ASSERT_EQ(tgt[0], ids[1]); + ASSERT_EQ(src[FN - 2], ids[FN - 2]); + ASSERT_EQ(tgt[FN - 2], ids[FN - 1]); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks + 2); + ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE) >= + bytes + 2 * (size_t)count * sizeof(int64_t)); + + cbm_store_free_call_edges(src, tgt); + ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE), bytes); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks); + cbm_store_close(s); + PASS(); +} + /* ── Louvain tests ──────────────────────────────────────────────── */ TEST(louvain_basic) { @@ -1564,7 +1622,8 @@ TEST(leiden_result_memory_core_ownership) { ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1); ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + 3 * sizeof(*result)); - cbm_free(CBM_MEM_CLASS_OTHER, result); + /* Released through the exported pair, the way an FFI caller must. */ + cbm_leiden_free(result); ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes); ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks); } @@ -2014,6 +2073,7 @@ SUITE(store_arch) { RUN_TEST(fetch_call_edges_basic); RUN_TEST(fetch_call_edges_truncated); RUN_TEST(fetch_call_edges_empty_project); + RUN_TEST(fetch_call_edges_memory_core_ownership); /* Louvain */ RUN_TEST(louvain_basic); diff --git a/tests/test_ui.c b/tests/test_ui.c index 4d31410d9..8c3a60006 100644 --- a/tests/test_ui.c +++ b/tests/test_ui.c @@ -562,6 +562,40 @@ TEST(layout_to_json) { PASS(); } +/* cbm_layout_to_json hands out one memory-core block of the OTHER class, and + * cbm_layout_free_json takes exactly that block back. A raw allocation on either + * side of the pair fails the count. */ +TEST(layout_json_memory_core_ownership) { + cbm_store_t *store = cbm_store_open_memory(); + ASSERT_NOT_NULL(store); + cbm_store_upsert_project(store, "test", "/tmp/test"); + cbm_node_t n = {.project = "test", + .label = "Function", + .name = "hello", + .qualified_name = "test::hello", + .file_path = "a.c", + .start_line = 1, + .end_line = 5}; + cbm_store_upsert_node(store, &n); + cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100); + ASSERT_NOT_NULL(r); + + size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER); + size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER); + char *json = cbm_layout_to_json(r); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"hello\"") != NULL); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1); + ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + strlen(json) + 1); + + cbm_layout_free_json(json); + ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks); + cbm_layout_free(r); + cbm_store_close(store); + PASS(); +} + TEST(layout_null_inputs) { /* NULL store → NULL result */ cbm_layout_result_t *r = cbm_layout_compute(NULL, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100); @@ -2698,6 +2732,7 @@ SUITE(ui) { RUN_TEST(layout_honors_budget_above_default); RUN_TEST(layout_deterministic); RUN_TEST(layout_to_json); + RUN_TEST(layout_json_memory_core_ownership); RUN_TEST(layout_null_inputs); RUN_TEST(layout_dead_code_classification); RUN_TEST(layout_coincident_nodes_bounded);