Break-glass merge PR #2597

This commit is contained in:
Martin Vogel
2026-10-12 20:15:14 +02:00
committed by GitHub
72 changed files with 20687 additions and 3578 deletions
+431 -120
View File
@@ -105,6 +105,11 @@ struct cbm_gbuf {
bool by_id_off;
int64_t by_id_cap;
/* Same-QN collision spans, written on export (cbm_gbuf_settle_variants).
* key: the surviving node's own QN string, value: gb_span_vec_t*. NULL
* until the first collision. */
CBMHashTable *variant_spans;
/* Secondary node indexes */
CBMHashTable *nodes_by_label; /* key: label, value: (node_ptr_array_t*) */
CBMHashTable *nodes_by_name; /* key: name, value: (node_ptr_array_t*) */
@@ -672,10 +677,16 @@ cbm_gbuf_t *cbm_gbuf_new_worker(const char *project, const char *root_path,
return gb;
}
static void gb_free_span_vec(const char *qn, void *value, void *ud);
void cbm_gbuf_free(cbm_gbuf_t *gb) {
if (!gb) {
return;
}
if (gb->variant_spans) {
cbm_ht_foreach(gb->variant_spans, gb_free_span_vec, NULL);
cbm_ht_free(gb->variant_spans);
}
/* Free each individually-allocated node */
for (int i = 0; i < gb->nodes.count; i++) {
@@ -897,14 +908,32 @@ static void gb_yy_free(void *ctx, void *block) {
static const yyjson_alc gb_yy_alc = {gb_yy_malloc, gb_yy_realloc, gb_yy_free, NULL};
enum { GB_VARIANTS_MAX = 64 };
typedef struct {
const char *file;
const char *file; /* interned in the buffer's pool */
int start;
int end;
} gb_span_t;
/* One side of a same-QN collision. */
typedef struct {
const char *label;
const char *props;
const char *file;
int start;
int end;
} gb_def_t;
/* Every definition span that collapsed into one node, gathered while the
* buffer is built and written into the node's `variants` once, on export
* (cbm_gbuf_settle_variants). Rewriting a growing JSON list on every
* collision was quadratic in the collisions of one QN: 7,586 junit
* `testcase` elements in one file. */
typedef struct {
gb_span_t *items;
size_t count;
size_t cap;
} gb_span_vec_t;
static bool gb_variant_label(const char *label) {
static const char *const labels[] = {"Function", "Method", "Class", "Interface", "Enum",
"Struct", "Trait", "Type", "Macro"};
@@ -929,99 +958,260 @@ static int gb_span_cmp(const void *a, const void *b) {
return c;
}
/* A definition's spans: its `variants` property, else its own span. */
static int gb_spans_of(yyjson_doc *doc, const char *file, int start, int end, gb_span_t *out,
int n) {
/* The non-empty `variants` list of a properties document, or NULL. */
static yyjson_val *gb_variants_list(yyjson_doc *doc) {
yyjson_val *list = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "variants") : NULL;
if (yyjson_is_arr(list) && yyjson_arr_size(list) > 0) {
return yyjson_is_arr(list) && yyjson_arr_size(list) > 0 ? list : NULL;
}
static bool gb_props_have_variants(const char *props) {
return props && strstr(props, "\"variants\"") != NULL;
}
/* Does this side carry a span: a variants list, or a definition's own span? */
static bool gb_def_has_spans(const gb_def_t *d) {
return gb_props_have_variants(d->props) || (gb_variant_label(d->label) && d->start > 0);
}
static void gb_span_push(gb_span_vec_t *vec, gb_span_t span) {
if (vec->count == vec->cap) {
enum { GB_SPANS_MIN = 4 };
size_t cap = vec->cap ? vec->cap * PAIR_LEN : GB_SPANS_MIN;
gb_span_t *grown = cbm_realloc(CBM_MEM_CLASS_GBUF_INDEX, vec->items, cap * sizeof(*grown));
if (!grown) {
return; /* a lost span costs precision, never data */
}
vec->items = grown;
vec->cap = cap;
}
vec->items[vec->count++] = span;
}
/* Append a side's spans: its `variants` list, else its own span when it is a
* definition. File paths are interned, so the spans outlive the props. */
static void gb_collect_spans(cbm_gbuf_t *gb, gb_span_vec_t *vec, const gb_def_t *d) {
if (gb_props_have_variants(d->props)) {
yyjson_doc *doc = yyjson_read_opts((char *)d->props, strlen(d->props), 0, &gb_yy_alc, NULL);
yyjson_val *list = gb_variants_list(doc);
if (list) {
size_t i;
size_t max;
yyjson_val *v;
yyjson_arr_foreach(list, i, max, v) {
const char *f = yyjson_get_str(yyjson_obj_get(v, "file_path"));
int st = (int)yyjson_get_int(yyjson_obj_get(v, "start_line"));
int en = (int)yyjson_get_int(yyjson_obj_get(v, "end_line"));
if (f && st > 0) {
gb_span_push(vec, (gb_span_t){gb_intern(gb, f), st, en});
}
}
yyjson_doc_free(doc);
return;
}
yyjson_doc_free(doc);
}
if (gb_variant_label(d->label) && d->start > 0) {
gb_span_push(vec, (gb_span_t){gb_intern(gb, d->file ? d->file : ""), d->start, d->end});
}
}
/* Append spans gathered in another buffer (a worker's), re-interned here. */
static void gb_append_spans(cbm_gbuf_t *gb, gb_span_vec_t *vec, const gb_span_vec_t *from) {
for (size_t i = 0; i < from->count; i++) {
gb_span_t span = from->items[i];
span.file = gb_intern(gb, span.file);
gb_span_push(vec, span);
}
}
/* A same-QN collision: gather both sides' spans for the surviving node,
* whichever label wins and however many there are. Call it before the
* winner is decided, while `existing` still holds its own fields.
* `arrival_spans` are the arrival's spans gathered in a worker buffer (they
* already include its own); NULL when it brings none. */
static void gb_note_collision(cbm_gbuf_t *gb, const cbm_gbuf_node_t *existing,
const gb_def_t *arrival, const gb_span_vec_t *arrival_spans) {
gb_def_t prior = {existing->label, existing->properties_json, existing->file_path,
existing->start_line, existing->end_line};
bool arrival_has = arrival_spans ? arrival_spans->count > 0 : gb_def_has_spans(arrival);
if (!existing->qualified_name || (!gb_def_has_spans(&prior) && !arrival_has)) {
return;
}
if (!gb->variant_spans) {
gb->variant_spans = cbm_ht_create_in(CBM_MEM_CLASS_GBUF_INDEX, 0);
if (!gb->variant_spans) {
return;
}
}
gb_span_vec_t *vec = cbm_ht_get(gb->variant_spans, existing->qualified_name);
if (!vec) {
vec = cbm_calloc(CBM_MEM_CLASS_GBUF_INDEX, sizeof(*vec));
if (!vec) {
return;
}
gb_collect_spans(gb, vec, &prior);
cbm_ht_set(gb->variant_spans, existing->qualified_name, vec);
}
if (arrival_spans) {
gb_append_spans(gb, vec, arrival_spans);
} else {
gb_collect_spans(gb, vec, arrival);
}
}
/* A worker node copied into this buffer brings the spans it gathered there. */
static void gb_adopt_spans(cbm_gbuf_t *gb, const cbm_gbuf_node_t *node, const gb_span_vec_t *from) {
if (!node || !node->qualified_name || !from || from->count == 0) {
return;
}
if (!gb->variant_spans) {
gb->variant_spans = cbm_ht_create_in(CBM_MEM_CLASS_GBUF_INDEX, 0);
if (!gb->variant_spans) {
return;
}
}
gb_span_vec_t *vec = cbm_ht_get(gb->variant_spans, node->qualified_name);
if (!vec) {
vec = cbm_calloc(CBM_MEM_CLASS_GBUF_INDEX, sizeof(*vec));
if (!vec) {
return;
}
cbm_ht_set(gb->variant_spans, node->qualified_name, vec);
}
gb_append_spans(gb, vec, from);
}
static void gb_span_vec_free(gb_span_vec_t *vec) {
if (vec) {
cbm_free(CBM_MEM_CLASS_GBUF_INDEX, vec->items);
cbm_free(CBM_MEM_CLASS_GBUF_INDEX, vec);
}
}
/* A node leaves the QN index: its gathered spans go with it, so a later node
* of the same QN starts from its own. */
static void gb_drop_variant_spans(cbm_gbuf_t *gb, const char *qn) {
if (gb->variant_spans && qn) {
gb_span_vec_free(cbm_ht_delete(gb->variant_spans, qn));
}
}
/* Write a node's gathered spans into its properties: sorted, distinct, the
* list left out only when it would say no more than the node's own span. */
static void gb_write_variants(cbm_gbuf_node_t *node, gb_span_vec_t *vec) {
qsort(vec->items, vec->count, sizeof(vec->items[0]), gb_span_cmp);
size_t unique = 0;
for (size_t i = 0; i < vec->count; i++) {
if (unique == 0 || gb_span_cmp(&vec->items[unique - SKIP_ONE], &vec->items[i]) != 0) {
vec->items[unique++] = vec->items[i];
}
}
gb_span_t own = {node->file_path ? node->file_path : "", node->start_line, node->end_line};
bool own_is_span = gb_variant_label(node->label) && node->start_line > 0;
bool only_own =
own_is_span ? unique == SKIP_ONE && gb_span_cmp(&vec->items[0], &own) == 0 : unique == 0;
if (only_own && !gb_props_have_variants(node->properties_json)) {
return;
}
/* Numbers stay as written: a pass printed them (importance 0.000000). */
const char *props = node->properties_json;
yyjson_doc *doc = props ? yyjson_read_opts((char *)props, strlen(props),
YYJSON_READ_NUMBER_AS_RAW, &gb_yy_alc, NULL)
: NULL;
yyjson_mut_doc *mut = yyjson_mut_doc_new(&gb_yy_alc);
yyjson_mut_val *root = NULL;
if (mut) {
root = doc && yyjson_is_obj(yyjson_doc_get_root(doc))
? yyjson_val_mut_copy(mut, yyjson_doc_get_root(doc))
: yyjson_mut_obj(mut);
}
yyjson_mut_val *list = root && !only_own ? yyjson_mut_arr(mut) : NULL;
for (size_t i = 0; list && i < unique; i++) {
yyjson_mut_val *v = yyjson_mut_arr_add_obj(mut, list);
yyjson_mut_obj_add_strcpy(mut, v, "file_path", vec->items[i].file);
yyjson_mut_obj_add_int(mut, v, "start_line", vec->items[i].start);
yyjson_mut_obj_add_int(mut, v, "end_line", vec->items[i].end);
}
if (root && (only_own || list)) {
yyjson_mut_obj_remove_key(root, "variants");
if (list) {
yyjson_mut_obj_add_val(mut, root, "variants", list);
}
yyjson_mut_doc_set_root(mut, root);
char *out = yyjson_mut_write_opts(mut, 0, &gb_yy_alc, NULL, NULL);
if (out) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, node->properties_json);
node->properties_json = out;
}
}
yyjson_mut_doc_free(mut);
yyjson_doc_free(doc);
}
static void gb_settle_one(const char *qn, void *value, void *ud) {
cbm_gbuf_t *gb = ud;
gb_span_vec_t *vec = value;
cbm_gbuf_node_t *node = gb->node_by_qn ? cbm_ht_get(gb->node_by_qn, qn) : NULL;
if (node && vec) {
gb_write_variants(node, vec);
}
gb_span_vec_free(vec);
}
static void gb_free_span_vec(const char *qn, void *value, void *ud) {
(void)qn;
(void)ud;
gb_span_vec_free(value);
}
void cbm_gbuf_foreach_span(const cbm_gbuf_t *gb, const cbm_gbuf_node_t *n, cbm_gbuf_span_fn fn,
void *ud) {
if (!gb || !n || !fn) {
return;
}
const gb_span_vec_t *vec = gb->variant_spans && n->qualified_name
? cbm_ht_get(gb->variant_spans, n->qualified_name)
: NULL;
if (vec && vec->count > 0) {
for (size_t i = 0; i < vec->count; i++) {
fn(vec->items[i].file, vec->items[i].start, vec->items[i].end, ud);
}
return;
}
if (gb_props_have_variants(n->properties_json)) {
yyjson_doc *doc = yyjson_read_opts((char *)n->properties_json, strlen(n->properties_json),
0, &gb_yy_alc, NULL);
yyjson_val *list = gb_variants_list(doc);
size_t i;
size_t max;
yyjson_val *v;
bool any = false;
yyjson_arr_foreach(list, i, max, v) {
const char *f = yyjson_get_str(yyjson_obj_get(v, "file_path"));
int s = (int)yyjson_get_int(yyjson_obj_get(v, "start_line"));
int e = (int)yyjson_get_int(yyjson_obj_get(v, "end_line"));
if (f && s > 0 && n < GB_VARIANTS_MAX) {
out[n++] = (gb_span_t){f, s, e};
if (f && s > 0) {
fn(f, s, e, ud);
any = true;
}
}
return n;
yyjson_doc_free(doc);
if (any) {
return;
}
}
if (start > 0 && n < GB_VARIANTS_MAX) {
out[n++] = (gb_span_t){file ? file : "", start, end};
if (n->file_path && n->start_line > 0) {
fn(n->file_path, n->start_line, n->end_line, ud);
}
return n;
}
/* The survivor's properties with the union of both definitions' variant
* spans. Returns a GBUF_STRING-owned JSON string, or NULL when there is
* nothing to record (fewer than two distinct spans) or on any failure — the
* survivor then keeps its own properties: a lost variants list costs
* precision, never data. */
static char *gb_variants_props(const char *survivor_props, const char *a_props, const char *a_file,
int a_start, int a_end, const char *b_props, const char *b_file,
int b_start, int b_end) {
if (a_start <= 0 || b_start <= 0) {
return NULL;
}
yyjson_doc *a_doc =
a_props ? yyjson_read_opts((char *)a_props, strlen(a_props), 0, &gb_yy_alc, NULL) : NULL;
yyjson_doc *b_doc =
b_props ? yyjson_read_opts((char *)b_props, strlen(b_props), 0, &gb_yy_alc, NULL) : NULL;
gb_span_t spans[GB_VARIANTS_MAX * 2];
int n = gb_spans_of(a_doc, a_file, a_start, a_end, spans, 0);
n = gb_spans_of(b_doc, b_file, b_start, b_end, spans, n);
qsort(spans, (size_t)n, sizeof(spans[0]), gb_span_cmp);
int unique = 0;
for (int i = 0; i < n; i++) {
if (unique == 0 || gb_span_cmp(&spans[unique - 1], &spans[i]) != 0) {
spans[unique++] = spans[i];
}
}
char *out = NULL;
if (unique >= 2) {
yyjson_mut_doc *mut = yyjson_mut_doc_new(&gb_yy_alc);
yyjson_doc *s_doc = survivor_props
? yyjson_read_opts((char *)survivor_props, strlen(survivor_props),
0, &gb_yy_alc, NULL)
: NULL;
yyjson_mut_val *root = s_doc && yyjson_is_obj(yyjson_doc_get_root(s_doc))
? yyjson_val_mut_copy(mut, yyjson_doc_get_root(s_doc))
: yyjson_mut_obj(mut);
yyjson_mut_val *list = yyjson_mut_arr(mut);
for (int i = 0; root && list && i < unique; i++) {
yyjson_mut_val *v = yyjson_mut_arr_add_obj(mut, list);
yyjson_mut_obj_add_strcpy(mut, v, "file_path", spans[i].file);
yyjson_mut_obj_add_int(mut, v, "start_line", spans[i].start);
yyjson_mut_obj_add_int(mut, v, "end_line", spans[i].end);
}
if (mut && root && list) {
yyjson_mut_obj_remove_key(root, "variants");
yyjson_mut_obj_add_val(mut, root, "variants", list);
yyjson_mut_doc_set_root(mut, root);
out = yyjson_mut_write_opts(mut, 0, &gb_yy_alc, NULL, NULL);
}
yyjson_doc_free(s_doc);
yyjson_mut_doc_free(mut);
}
yyjson_doc_free(a_doc);
yyjson_doc_free(b_doc);
return out;
}
/* Record the variant spans of a same-QN collision on the surviving node. */
static void gb_record_variants(cbm_gbuf_node_t *survivor, const char *a_label, const char *a_props,
const char *a_file, int a_start, int a_end, const char *b_label,
const char *b_props, const char *b_file, int b_start, int b_end) {
if (!gb_variant_label(a_label) || !b_label || strcmp(a_label, b_label) != 0) {
void cbm_gbuf_settle_variants(cbm_gbuf_t *gb) {
if (!gb || !gb->variant_spans) {
return;
}
char *merged = gb_variants_props(survivor->properties_json, a_props, a_file, a_start, a_end,
b_props, b_file, b_start, b_end);
if (merged) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, survivor->properties_json);
survivor->properties_json = merged;
}
cbm_ht_foreach(gb->variant_spans, gb_settle_one, gb);
cbm_ht_free(gb->variant_spans);
gb->variant_spans = NULL;
}
/* ── Node operations ─────────────────────────────────────────────── */
@@ -1099,20 +1289,13 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
if (c == 0) {
c = strcmp(existing->label ? existing->label : "", label ? label : "");
}
gb_def_t arrival = {label, properties_json, file_path, start_line, end_line};
gb_note_collision(gb, existing, &arrival, NULL);
if (c > 0) {
gb_record_variants(existing, existing->label, existing->properties_json,
existing->file_path, existing->start_line, existing->end_line, label,
properties_json, file_path, start_line, end_line);
return existing->id; /* existing entity is the canonical winner */
}
/* The arrival wins: its properties, plus every variant span both carry. */
char *variant_props = NULL;
if (gb_variant_label(label) && existing->label && strcmp(existing->label, label) == 0) {
variant_props =
gb_variants_props(properties_json, existing->properties_json, existing->file_path,
existing->start_line, existing->end_line, properties_json,
file_path, start_line, end_line);
}
/* The arrival wins: its properties; the variant spans both carry are
* written on export (cbm_gbuf_settle_variants). */
/* Update in-place. name/properties are strdup'd BEFORE freeing old ones
* (callers may pass existing->name as an argument). label/file_path are
* interned: gb_intern returns a stable pool pointer (idempotent even when
@@ -1122,9 +1305,7 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
* label/name — cbm_gbuf_find_by_label/name then missed or mis-listed it,
* which is how the flickering Function set reached the semantic pass). */
char *new_name = heap_strdup(name);
char *new_props = variant_props ? variant_props
: properties_json ? heap_strdup(properties_json)
: NULL;
char *new_props = properties_json ? heap_strdup(properties_json) : NULL;
const char *new_label_interned = gb_intern(gb, label);
bool label_changed = !existing->label || !new_label_interned ||
strcmp(existing->label, new_label_interned) != 0;
@@ -1269,6 +1450,7 @@ int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label) {
intptr_to_ptr(SKIP_ONE));
/* Remove from primary indexes */
gb_drop_variant_spans(gb, n->qualified_name);
cbm_ht_delete(gb->node_by_qn, n->qualified_name);
if (n->id >= 0 && n->id < gb->by_id_cap) {
gb->by_id[n->id] = NULL;
@@ -1316,6 +1498,7 @@ int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) {
remove_node_from_ptr_array(cbm_ht_get(gb->nodes_by_name, n->name), n->id);
/* Remove from primary indexes */
gb_drop_variant_spans(gb, n->qualified_name);
cbm_ht_delete(gb->node_by_qn, n->qualified_name);
if (n->id >= 0 && n->id < gb->by_id_cap) {
gb->by_id[n->id] = NULL;
@@ -1348,6 +1531,75 @@ int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) {
return deleted_count;
}
/* Whether an edge of `types` (NULL-terminated) touches node `id`. */
static bool gbuf_node_touched(const cbm_gbuf_t *gb, int64_t id, const char *const *types) {
for (int t = 0; types[t]; t++) {
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if ((cbm_gbuf_find_edges_by_target_type(gb, id, types[t], &edges, &count) == 0 &&
count > 0) ||
(cbm_gbuf_find_edges_by_source_type(gb, id, types[t], &edges, &count) == 0 &&
count > 0)) {
return true;
}
}
return false;
}
int cbm_gbuf_delete_unreferenced(cbm_gbuf_t *gb, const char *label, const char *const *edge_types) {
if (!gb || !label || !edge_types || !edge_types[0]) {
return CBM_NOT_FOUND;
}
node_ptr_array_t *arr = cbm_ht_get(gb->nodes_by_label, label);
if (!arr || arr->count == 0) {
return 0;
}
/* The victims first: the per-node unlinking below edits `arr`. */
cbm_gbuf_node_t **victims =
cbm_calloc(CBM_MEM_CLASS_GBUF_INDEX, (size_t)arr->count * sizeof(*victims));
if (!victims) {
return CBM_NOT_FOUND;
}
int n = 0;
for (int i = 0; i < arr->count; i++) {
const char *qn = arr->items[i]->qualified_name;
cbm_gbuf_node_t *node = qn ? cbm_ht_get(gb->node_by_qn, qn) : NULL;
if (node && !gbuf_node_touched(gb, node->id, edge_types)) {
victims[n++] = node;
}
}
if (n == 0) {
cbm_free(CBM_MEM_CLASS_GBUF_INDEX, victims);
return 0;
}
CBMHashTable *deleted_set = cbm_ht_create((uint32_t)n);
for (int i = 0; i < n; i++) {
cbm_gbuf_node_t *node = victims[i];
if (!node->qualified_name) {
continue;
}
char id_buf[CBM_SZ_32];
make_id_key(id_buf, sizeof(id_buf), node->id);
cbm_ht_set(deleted_set, cbm_mem_strdup(CBM_MEM_CLASS_GBUF_INDEX, id_buf),
intptr_to_ptr(SKIP_ONE));
remove_node_from_ptr_array(cbm_ht_get(gb->nodes_by_label, node->label), node->id);
remove_node_from_ptr_array(cbm_ht_get(gb->nodes_by_name, node->name), node->id);
gb_drop_variant_spans(gb, node->qualified_name);
cbm_ht_delete(gb->node_by_qn, node->qualified_name);
if (node->id >= 0 && node->id < gb->by_id_cap) {
gb->by_id[node->id] = NULL;
}
/* As delete_by_file: a NULL QN fails the dump's liveness check. */
cbm_free(CBM_MEM_CLASS_GBUF_STRING, node->qualified_name);
node->qualified_name = NULL;
}
cascade_delete_edges(gb, deleted_set);
cbm_ht_foreach(deleted_set, free_index_key, NULL);
cbm_ht_free(deleted_set);
cbm_free(CBM_MEM_CLASS_GBUF_INDEX, victims);
return n;
}
int cbm_gbuf_load_from_db(cbm_gbuf_t *gb, const char *db_path, const char *project) {
if (!gb || !db_path || !project) {
return CBM_NOT_FOUND;
@@ -1636,16 +1888,15 @@ int cbm_gbuf_edge_count_by_type(const cbm_gbuf_t *gb, const char *type) {
return arr ? arr->count : 0;
}
int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) {
if (!gb || !type) {
return CBM_NOT_FOUND;
}
/* Remove the edges `drop` selects from the array and the dedup index, then
* rebuild the secondary indexes. */
typedef bool (*gbuf_edge_pred_t)(const cbm_gbuf_t *gb, const cbm_gbuf_edge_t *e, const void *ud);
/* Remove edges of the given type from array and dedup index */
static int gbuf_delete_edges_if(cbm_gbuf_t *gb, gbuf_edge_pred_t drop, const void *ud) {
int write_idx = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
if (strcmp(e->type, type) == 0) {
if (drop(gb, e, ud)) {
char key[EDGE_KEY_BUF];
make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type,
e->properties_json);
@@ -1667,6 +1918,68 @@ int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) {
return 0;
}
static bool gbuf_edge_is_type(const cbm_gbuf_t *gb, const cbm_gbuf_edge_t *e, const void *ud) {
(void)gb;
return strcmp(e->type, (const char *)ud) == 0;
}
int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) {
if (!gb || !type) {
return CBM_NOT_FOUND;
}
return gbuf_delete_edges_if(gb, gbuf_edge_is_type, type);
}
typedef struct {
const char *type;
const char *label;
} gbuf_type_label_t;
static bool gbuf_edge_from_label(const cbm_gbuf_t *gb, const cbm_gbuf_edge_t *e, const void *ud) {
const gbuf_type_label_t *tl = ud;
if (strcmp(e->type, tl->type) != 0) {
return false;
}
const cbm_gbuf_node_t *src = cbm_gbuf_find_by_id(gb, e->source_id);
return src && src->label && strcmp(src->label, tl->label) == 0;
}
int cbm_gbuf_delete_edges_from_label(cbm_gbuf_t *gb, const char *type, const char *label) {
if (!gb || !type || !label) {
return CBM_NOT_FOUND;
}
gbuf_type_label_t tl = {type, label};
return gbuf_delete_edges_if(gb, gbuf_edge_from_label, &tl);
}
static int gbuf_ptr_cmp(const void *k, const void *v) {
uintptr_t a = (uintptr_t)*(const cbm_gbuf_edge_t *const *)k;
uintptr_t b = (uintptr_t)*(const cbm_gbuf_edge_t *const *)v;
return (a > b) - (a < b);
}
typedef struct {
const cbm_gbuf_edge_t **edges;
int n;
} gbuf_edge_set_t;
static bool gbuf_edge_in_set(const cbm_gbuf_t *gb, const cbm_gbuf_edge_t *e, const void *ud) {
(void)gb;
const gbuf_edge_set_t *s = ud;
return bsearch(&e, s->edges, (size_t)s->n, sizeof(*s->edges), gbuf_ptr_cmp) != NULL;
}
/* By the edges themselves, not their ids: after the parallel resolve merge,
* cbm_gbuf_set_next_id can hand an id out twice. */
int cbm_gbuf_delete_edges(cbm_gbuf_t *gb, const cbm_gbuf_edge_t **edges, int n) {
if (!gb || !edges || n <= 0) {
return 0;
}
qsort((void *)edges, (size_t)n, sizeof(*edges), gbuf_ptr_cmp);
gbuf_edge_set_t s = {edges, n};
return gbuf_delete_edges_if(gb, gbuf_edge_in_set, &s);
}
/* ── Merge ───────────────────────────────────────────────────────── */
/* Free remap hash table entries (key = heap string, value = heap int64_t*) */
@@ -1679,7 +1992,8 @@ static void free_remap_entry(const char *key, void *val, void *ud) {
/* Handle QN collision: update dst node fields (src wins), record remap if IDs differ.
* label/file_path are re-interned into dst's pool (sn's pointers belong to src). */
static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
const cbm_gbuf_node_t *sn, CBMHashTable **remap) {
const cbm_gbuf_node_t *sn, const gb_span_vec_t *sn_spans,
CBMHashTable **remap) {
/* Same guard as cbm_gbuf_upsert_node: a per-file "Module" def coming from a
* worker-local gbuf must not touch the structural directory node ("Project"
* root or "Folder") that shares its QN in a directory-based-module language
@@ -1718,22 +2032,12 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
c = strcmp(existing->label ? existing->label : "", sn->label ? sn->label : "");
}
bool sn_wins = c <= 0;
if (!sn_wins) {
gb_record_variants(existing, existing->label, existing->properties_json,
existing->file_path, existing->start_line, existing->end_line,
sn->label, sn->properties_json, sn->file_path, sn->start_line,
sn->end_line);
}
gb_def_t src_def = {sn->label, sn->properties_json, sn->file_path, sn->start_line,
sn->end_line};
gb_note_collision(dst, existing, &src_def, sn_spans);
if (sn_wins) {
/* src's properties, plus every variant span both carry. */
char *variant_props = NULL;
if (gb_variant_label(sn->label) && existing->label &&
strcmp(existing->label, sn->label) == 0) {
variant_props = gb_variants_props(sn->properties_json, existing->properties_json,
existing->file_path, existing->start_line,
existing->end_line, sn->properties_json,
sn->file_path, sn->start_line, sn->end_line);
}
/* src's properties; the variant spans both carry are written on
* export (cbm_gbuf_settle_variants). */
/* Keep the secondary indexes consistent when the surviving
* label/name changes (the old code left the node listed under its
* original label/name, so find_by_label/name mis-listed it). */
@@ -1758,10 +2062,9 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
existing->file_path = (char *)gb_intern(dst, sn->file_path);
existing->start_line = sn->start_line;
existing->end_line = sn->end_line;
if (variant_props || sn->properties_json) {
if (sn->properties_json) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, existing->properties_json);
existing->properties_json =
variant_props ? variant_props : heap_strdup(sn->properties_json);
existing->properties_json = heap_strdup(sn->properties_json);
}
if (label_changed) {
node_ptr_array_t *by_label = get_or_create_node_array(
@@ -1862,11 +2165,16 @@ int cbm_gbuf_merge(cbm_gbuf_t *dst, cbm_gbuf_t *src) {
continue;
}
/* The spans src gathered for this node travel with it; they are
* written once, when dst is exported. */
const gb_span_vec_t *spans =
src->variant_spans ? cbm_ht_get(src->variant_spans, sn->qualified_name) : NULL;
cbm_gbuf_node_t *existing = cbm_ht_get(dst->node_by_qn, sn->qualified_name);
if (existing) {
merge_update_existing(dst, existing, sn, &remap);
merge_update_existing(dst, existing, sn, spans, &remap);
} else {
merge_copy_new_node(dst, sn);
gb_adopt_spans(dst, cbm_ht_get(dst->node_by_qn, sn->qualified_name), spans);
}
}
@@ -2155,6 +2463,7 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
if (!gb || !path) {
return CBM_NOT_FOUND;
}
cbm_gbuf_settle_variants(gb);
CBM_PROF_START(t_count);
int live_count = count_live_nodes(gb);
@@ -2272,6 +2581,7 @@ int cbm_gbuf_flush_to_store(cbm_gbuf_t *gb, cbm_store_t *store) {
if (!gb || !store) {
return CBM_NOT_FOUND;
}
cbm_gbuf_settle_variants(gb);
/* Upsert project */
cbm_store_upsert_project(store, gb->project, gb->root_path);
@@ -2346,6 +2656,7 @@ int cbm_gbuf_merge_into_store(cbm_gbuf_t *gb, cbm_store_t *store) {
if (!gb || !store) {
return CBM_NOT_FOUND;
}
cbm_gbuf_settle_variants(gb);
/* Begin bulk mode — no project wipe */
cbm_store_begin(store);
+27
View File
@@ -117,6 +117,13 @@ int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label);
* Used by incremental indexing to remove stale nodes before re-extraction. */
int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path);
/* Delete every `label` node that no edge of `edge_types` (NULL-terminated)
* touches, as source or target (with any edges it still has); returns how
* many. For nodes that exist only through such edges and carry no file
* (Process, Image): incremental indexing drops them when their last referrer
* is gone, as a full index never makes them. */
int cbm_gbuf_delete_unreferenced(cbm_gbuf_t *gb, const char *label, const char *const *edge_types);
/* Bulk-load all nodes and edges for a project from an existing SQLite DB
* into this graph buffer. Returns 0 on success. */
int cbm_gbuf_load_from_db(cbm_gbuf_t *gb, const char *db_path, const char *project);
@@ -159,6 +166,13 @@ int cbm_gbuf_edge_count_by_type(const cbm_gbuf_t *gb, const char *type);
/* Delete all edges of a type. */
int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type);
/* Delete the edges of `type` whose source node has label `label`. */
int cbm_gbuf_delete_edges_from_label(cbm_gbuf_t *gb, const char *type, const char *label);
/* Delete edges[0..n) (pointers found in this buffer; the array is sorted in
* place). */
int cbm_gbuf_delete_edges(cbm_gbuf_t *gb, const cbm_gbuf_edge_t **edges, int n);
/* ── Vector storage (for semantic embeddings) ───────────────────── */
/* Store an int8-quantized vector for a node. The vector data is copied.
@@ -195,4 +209,17 @@ int cbm_gbuf_merge_into_store(cbm_gbuf_t *gb, cbm_store_t *store);
* (the old value is kept). */
int cbm_gbuf_node_set_properties_json(cbm_gbuf_node_t *node, const char *json);
/* Write the definition spans gathered from same-QN collisions into each
* surviving node's `variants` property. The dump, flush and merge entry
* points do it themselves; call it directly only to read a node's
* properties_json from the buffer after collisions. */
void cbm_gbuf_settle_variants(cbm_gbuf_t *gb);
/* Visit every definition span node `n` stands for: the spans gathered from
* same-QN collisions (not yet written, or already in its `variants`), else
* its own. For passes that place a source line inside a definition. */
typedef void (*cbm_gbuf_span_fn)(const char *file, int start, int end, void *ud);
void cbm_gbuf_foreach_span(const cbm_gbuf_t *gb, const cbm_gbuf_node_t *n, cbm_gbuf_span_fn fn,
void *ud);
#endif /* CBM_GRAPH_BUFFER_H */