Files
codebase-memory-mcp/src/pipeline/pass_parallel.c
T

4898 lines
232 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pass_parallel.c — Three-phase parallel pipeline.
*
* Phase 3A: Parallel extract + create definition nodes (per-worker gbufs)
* Phase 3B: Serial registry build + edge creation from cached results
* Phase 4: Parallel call/usage/semantic resolution (per-worker edge bufs)
*
* Each file is read and parsed ONCE (Phase 3A). The CBMFileResult is cached
* and reused for resolution (Phase 4), eliminating 3x redundant I/O + parsing.
*
* Depends on: worker_pool, graph_buffer (shared IDs + merge), extraction (cbm.h)
*/
#include "foundation/constants.h"
enum {
PP_RING = 4,
PP_RING_MASK = 3,
PP_JSON_MARGIN = 10,
PP_ESC_MARGIN = 3,
PP_ESC_SPACE = 2,
/* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...]
* -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */
PP_JSON_FIELD_OVERHEAD = 6,
PP_ARGS_MARGIN = 20,
/* ,"line":<int> -> comma + key (7) + colon + up to 10 digits + NUL. */
PP_LINE_MARGIN = 24,
PP_LOG_THRESH = 24,
PP_LOG_INTERVAL = 10,
PP_TIMER_THRESH = 1000,
/* Extraction memory back-pressure: when the process is over its RSS budget,
* a worker reclaims + naps before pulling another file so peers can finish
* and return pages. Bounded spins avoid deadlock when the resident graph
* itself is near budget: then ONE confirmation cycle decides — drained
* means continue, still over means the attempt fails whole (decision A,
* #1997 #832) instead of the former open-ended soft overshoot. */
PP_BACKPRESSURE_MAX_SPINS = 40,
PP_BACKPRESSURE_NAP_NS = 3000000, /* 3 ms */
};
#define PP_NSEC_PER_SEC 1000000000ULL
#define PP_USEC_PER_MS 1000000ULL
#define PP_HALF_CONF 0.5
#define PP_FIELD_HINT_CONF 0.85
enum { PP_CSHARP_M_PREFIX_LEN = 2 };
/* Absolute source-retention ceilings for the parallel extract pipeline.
*
* The extract worker copies each file's source bytes into result->arena so
* the fused cross-file LSP step in resolve_worker can re-parse without
* re-opening the file. That retention is TRANSIENT (freed at run end) but it
* is a PEAK-RSS driver: every retained byte is resident at once across the
* extract→resolve handoff.
*
* A cap here is a FLOOR as much as a ceiling: whatever total we allow, we
* WILL hold that much resident at peak on a large repo. rust-analyzer bounds
* retained file *text* to a small fixed budget and re-reads source on a miss
* rather than scaling the retained set with host RAM — because the re-read is
* cheap relative to holding tens of GB resident. We follow the same model:
* - derive the total budget from the process memory budget (budget/8),
* - BUT clamp the RAM-derived DEFAULT to a small absolute ceiling (1 GiB)
* so a 512 GiB host does not retain 64 GiB of source it would re-read
* cheaply anyway, and keep the per-file cap modest (32 MiB) so one
* pathological generated blob cannot monopolise the budget.
* A file dropped from retention is NOT lost to cross-file resolution:
* resolve_worker re-reads it on demand, bounded and freed immediately
* (source_reread fallback). Both caps are env-overridable — CBM_RETAIN_TOTAL_MB
* / CBM_RETAIN_PER_FILE_MB raise or lower the auto-derived defaults directly
* (the hard ceilings bound only the RAM-derived default, never a deliberate
* operator/caller choice). */
#define PP_RETAIN_TOTAL_HARD_MAX_BYTES (1024ULL * 1024 * 1024) /* 1 GiB default ceiling */
#define PP_RETAIN_PER_FILE_HARD_MAX_BYTES (32ULL * 1024 * 1024) /* 32 MiB per file */
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/doc_links.h" /* cbm_doclinks_resolve_file: MENTIONS beside CALLS */
#include "result_spill.h"
#include "foundation/platform.h" /* cbm_resolve_cache_dir */
#include "pipeline/pass_lsp_cross.h" /* cbm_pxc_* helpers for fused cross-file LSP */
#include "pipeline/lsp_resolve.h"
#include "pipeline/laravel_routing.h"
#include "lsp/rust_cargo.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.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_fs.h"
#include "foundation/compat_thread.h"
#include "foundation/mem_core.h"
#include "graph_buffer/graph_buffer.h"
#include "service_patterns.h"
#include "foundation/platform.h"
#include "foundation/hash_table.h"
#include "foundation/log.h"
#include "foundation/slab_alloc.h"
#include "foundation/mem.h"
#include "foundation/str_util.h"
#include "foundation/profile.h"
#include "foundation/compat_regex.h"
#include "foundation/limits.h"
#include "cbm.h"
#include "arena.h"
#include "macro_table.h"
#include "simhash/minhash.h"
#include "semantic/ast_profile.h"
#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Back-pressure nap-cycle counter (test observability): each execution of the
* over-budget collect+nap gate counts one cycle. Lets tests assert the gate does
* not re-pay the full nap tax on every file pull when napping cannot reclaim
* memory (the resident floor, not in-flight transients, holds the budget). */
static _Atomic long g_bp_nap_cycles = 0;
static _Atomic uint64_t g_lsp_linear_fallback_rows = 0;
/* Defined here, declared in lsp_resolve.h — the uncapped tail-match scan's
* cost, surfaced at end of resolve (#1669). */
_Atomic uint64_t g_lsp_tail_lookups = 0;
_Atomic uint64_t g_lsp_tail_candidates = 0;
long cbm_pp_bp_nap_cycles(void) {
return atomic_load_explicit(&g_bp_nap_cycles, memory_order_relaxed);
}
void cbm_pp_bp_nap_cycles_reset(void) {
atomic_store_explicit(&g_bp_nap_cycles, 0, memory_order_relaxed);
}
uint64_t cbm_pp_lsp_linear_fallback_rows(void) {
return atomic_load_explicit(&g_lsp_linear_fallback_rows, memory_order_relaxed);
}
void cbm_pp_lsp_linear_fallback_rows_reset(void) {
atomic_store_explicit(&g_lsp_linear_fallback_rows, 0, memory_order_relaxed);
atomic_store_explicit(&g_lsp_tail_lookups, 0, memory_order_relaxed);
atomic_store_explicit(&g_lsp_tail_candidates, 0, memory_order_relaxed);
}
/* Parse a positive MB-valued retention env knob (CBM_RETAIN_*_MB) into bytes.
* Follows the limits.c strtol convention: unset / unparseable / non-positive
* → return 0 so the caller keeps its derived default. */
static size_t cbm_retain_env_bytes(const char *name) {
const char *raw = cbm_runtime_getenv(name);
if (!raw || !raw[0]) {
return 0;
}
errno = 0;
char *end = NULL;
long v = strtol(raw, &end, 10);
if (errno != 0 || end == raw || *end != '\0' || v <= 0) {
return 0;
}
return (size_t)v * 1024 * 1024;
}
/* Auto-derived TOTAL retention budget: a fraction of the process memory
* budget, clamped to the absolute ceiling. The clamp bounds ONLY this
* RAM-derived default — a huge-RAM host must not retain tens of GB it would
* re-read cheaply. When the budget is unset (tests / no cgroup) fall back to
* the ceiling. */
static size_t cbm_parallel_extract_default_total_cap(void) {
size_t cap = cbm_mem_budget();
if (cap > 0) {
cap /= 8;
} else {
cap = PP_RETAIN_TOTAL_HARD_MAX_BYTES;
}
if (cap > PP_RETAIN_TOTAL_HARD_MAX_BYTES) {
cap = PP_RETAIN_TOTAL_HARD_MAX_BYTES;
}
return cap;
}
/* Auto-derived PER-FILE cap: modest absolute ceiling, never above the total. */
static size_t cbm_parallel_extract_default_per_file_cap(size_t total_cap) {
size_t cap = PP_RETAIN_PER_FILE_HARD_MAX_BYTES;
if (cap > total_cap) {
cap = total_cap;
}
return cap;
}
/* Resolve the effective retention options from (in precedence order):
* explicit caller opts > CBM_RETAIN_*_MB env knobs > RAM-derived defaults.
* The absolute hard ceilings apply only to the RAM-derived defaults; an
* operator/caller that sets a value explicitly is trusted. The only invariant
* enforced unconditionally is per-file ≤ total. */
static cbm_parallel_extract_opts_t cbm_parallel_extract_resolve_opts(
const cbm_parallel_extract_opts_t *opts) {
cbm_parallel_extract_opts_t resolved = {
.retain_sources = true,
.retain_sources_set = true,
.retain_total_budget_bytes = cbm_parallel_extract_default_total_cap(),
.retain_per_file_max_bytes = 0,
};
size_t env_total = cbm_retain_env_bytes("CBM_RETAIN_TOTAL_MB");
if (env_total > 0) {
resolved.retain_total_budget_bytes = env_total;
}
resolved.retain_per_file_max_bytes =
cbm_parallel_extract_default_per_file_cap(resolved.retain_total_budget_bytes);
size_t env_per_file = cbm_retain_env_bytes("CBM_RETAIN_PER_FILE_MB");
if (env_per_file > 0) {
resolved.retain_per_file_max_bytes = env_per_file;
}
if (opts) {
if (opts->retain_sources_set) {
resolved.retain_sources = opts->retain_sources;
}
if (opts->retain_total_budget_bytes > 0) {
resolved.retain_total_budget_bytes = opts->retain_total_budget_bytes;
}
if (opts->retain_per_file_max_bytes > 0) {
resolved.retain_per_file_max_bytes = opts->retain_per_file_max_bytes;
}
}
/* Correctness invariant: a single file can never exceed the total budget. */
if (resolved.retain_per_file_max_bytes > resolved.retain_total_budget_bytes) {
resolved.retain_per_file_max_bytes = resolved.retain_total_budget_bytes;
}
return resolved;
}
static uint64_t extract_now_ns(void) {
struct timespec ts;
cbm_clock_gettime(CLOCK_MONOTONIC, &ts);
return ((uint64_t)ts.tv_sec * PP_NSEC_PER_SEC) + (uint64_t)ts.tv_nsec;
}
/* ── Helpers (duplicated from pass files — kept static for isolation) ── */
/* Read file into a malloc'd buffer (= mimalloc in production).
* *out_size receives the on-disk size and *out_status the failure reason so the
* caller can attribute a skip to the right phase (read vs oversized) instead of
* a silent drop. Both out params may be NULL. */
static char *read_file(const char *path, int *out_len, long *out_size,
cbm_read_status_t *out_status) {
if (out_size) {
*out_size = 0;
}
if (out_status) {
*out_status = CBM_READ_OK;
}
FILE *f = cbm_fopen(path, "rb");
if (!f) {
if (out_status) {
*out_status = CBM_READ_OPEN_FAIL;
}
return NULL;
}
(void)fseek(f, 0, SEEK_END);
long size = ftell(f);
(void)fseek(f, 0, SEEK_SET);
if (out_size) {
*out_size = size;
}
if (size <= 0) {
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_EMPTY;
}
return NULL;
}
if (size > cbm_max_file_bytes()) { /* generous, env-configurable cap (B4) */
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_OVERSIZED;
}
return NULL;
}
char *buf = (char *)malloc((size_t)size + SKIP_ONE);
if (!buf) {
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_OOM;
}
return NULL;
}
size_t nread = fread(buf, SKIP_ONE, (size_t)size, f);
(void)fclose(f);
buf[nread] = '\0';
*out_len = (int)nread;
return buf;
}
/* ── Per-worker skip list (Stage 2 / Track B) ───────────────────────
* Each extract worker appends read/extract/oversized skips into its OWN list
* (no lock on the hot path); the lists are merged into the pipeline's
* cbm_file_error_t array in the existing sequential merge loop. */
typedef struct {
cbm_file_error_t *items;
int count;
int cap;
} pp_err_list_t;
/* NULL-safe heap strdup. */
static char *pp_err_dup(const char *s) {
if (!s) {
return NULL;
}
size_t n = strlen(s) + 1;
char *d = (char *)malloc(n);
if (d) {
memcpy(d, s, n);
}
return d;
}
static void pp_err_add(pp_err_list_t *list, const char *path, const char *reason,
const char *phase) {
if (!list) {
return;
}
if (list->count >= list->cap) {
int ncap = list->cap ? list->cap * 2 : 8;
cbm_file_error_t *grown =
(cbm_file_error_t *)realloc(list->items, (size_t)ncap * sizeof(*grown));
if (!grown) {
return; /* drop on OOM — never fail extraction to record a skip */
}
list->items = grown;
list->cap = ncap;
}
list->items[list->count].path = pp_err_dup(path);
list->items[list->count].reason = pp_err_dup(reason);
list->items[list->count].phase = pp_err_dup(phase);
list->count++;
}
/* Free source buffer. */
static void free_source(char *buf) {
free(buf);
}
static const char *itoa_log(int val) {
static CBM_TLS char bufs[PP_RING][CBM_SZ_32];
static CBM_TLS int idx = 0;
int i = idx;
idx = (idx + SKIP_ONE) & PP_RING_MASK;
snprintf(bufs[i], sizeof(bufs[i]), "%d", val);
return bufs[i];
}
/* Append a JSON-escaped string value to buf at position *pos. */
/* Escape one character for JSON. Returns bytes written (1 or 2). */
static int json_escape_char(char *buf, size_t avail, char ch) {
char esc = 0;
switch (ch) {
case '"':
esc = '"';
break;
case '\\':
esc = '\\';
break;
case '\n':
esc = 'n';
break;
case '\r':
esc = 'r';
break;
case '\t':
esc = 't';
break;
default:
if (avail >= SKIP_ONE) {
/* Any other raw control byte (e.g. form feed) is invalid inside a
* JSON string — degrade to a space. */
buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch;
}
return SKIP_ONE;
}
if (avail >= PP_ESC_SPACE) {
buf[0] = '\\';
buf[SKIP_ONE] = esc;
}
return PP_ESC_SPACE;
}
/* Escaped length of a string under json_escape_char's rules: escaped
* characters expand to 2 bytes, everything else stays 1. */
static size_t pp_json_escaped_len(const char *s) {
size_t n = 0;
for (; *s; s++) {
switch (*s) {
case '"':
case '\\':
case '\n':
case '\r':
case '\t':
n += PP_ESC_SPACE;
break;
default:
n += SKIP_ONE;
}
}
return n;
}
/* Appends are ATOMIC: a field is emitted only if the WHOLE serialized form
* fits (with PP_ESC_SPACE bytes reserved for the closing '}' + NUL). Cutting a
* field mid-value produced unterminated strings/arrays — malformed properties
* JSON that aborts every json_extract()-based consumer downstream (seen on the
* Linux kernel: 50-param functions truncated at the 2 KB cap). Dropping an
* oversized optional field whole keeps the JSON valid. Twin of
* pass_definitions.c — keep both in sync. */
static void append_json_string(char *buf, size_t bufsize, size_t *pos, const char *key,
const char *val) {
if (!val || val[0] == '\0') {
return;
}
size_t required = strlen(key) + pp_json_escaped_len(val) + PP_JSON_FIELD_OVERHEAD;
if (*pos + required + PP_ESC_SPACE > bufsize) {
return; /* whole field would not fit — skip it atomically */
}
size_t p = *pos;
int w = snprintf(buf + p, bufsize - p, ",\"%s\":\"", key);
if (w <= 0 || (size_t)w >= bufsize - p) {
return;
}
p += (size_t)w;
for (const char *s = val; *s && p < bufsize - PP_ESC_MARGIN; s++) {
int n = json_escape_char(buf + p, bufsize - p - PP_ESC_SPACE, *s);
p += (size_t)n;
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
buf[p] = '\0';
*pos = p;
}
/* Append a JSON array of strings: ,"key":["a","b","c"]. Atomic like
* append_json_string: emitted only if the whole array fits. */
static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const char *key,
const char **arr) {
if (!arr || !arr[0] || *pos >= bufsize - PP_JSON_MARGIN) {
return;
}
/* ,"key":[ + per item "<escaped>" + separating commas + ] */
size_t required = strlen(key) + PP_JSON_FIELD_OVERHEAD;
for (int i = 0; arr[i]; i++) {
required += pp_json_escaped_len(arr[i]) + PP_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
}
if (*pos + required + PP_ESC_SPACE > bufsize) {
return; /* whole array would not fit — skip it atomically */
}
size_t p = *pos;
int n = snprintf(buf + p, bufsize - p, ",\"%s\":[", key);
if (n <= 0 || p + (size_t)n >= bufsize - PP_ESC_SPACE) {
return;
}
p += (size_t)n;
for (int i = 0; arr[i]; i++) {
if (i > 0 && p < bufsize - SKIP_ONE) {
buf[p++] = ',';
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
/* Full escaping (not just quote/backslash): items like C param types
* sliced from multi-line declarations carry raw \n/\t bytes, which are
* invalid inside JSON strings. */
for (const char *s = arr[i]; *s && p < bufsize - PP_ESC_SPACE; s++) {
p += (size_t)json_escape_char(buf + p, bufsize - p - PP_ESC_SPACE, *s);
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = ']';
}
buf[p] = '\0';
*pos = p;
}
/* Append an already-serialized JSON value verbatim: ,"key":<json>. Atomic like
* append_json_string. For `variants`, which extraction builds as a JSON array
* of objects (cbm.h). Twin of pass_definitions.c -- keep both in sync. */
static void append_json_raw(char *buf, size_t bufsize, size_t *pos, const char *key,
const char *json) {
if (!json || json[0] == '\0') {
return;
}
size_t required = strlen(key) + strlen(json) + PP_JSON_FIELD_OVERHEAD;
if (*pos + required + PP_ESC_SPACE > bufsize) {
return; /* whole field would not fit — skip it atomically */
}
int w = snprintf(buf + *pos, bufsize - *pos, ",\"%s\":%s", key, json);
if (w > 0 && (size_t)w < bufsize - *pos) {
*pos += (size_t)w;
}
}
static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) {
/* Complexity/loop/recursion metrics are meaningful only for Function/Method.
* Gate the block so the millions of Macro/Field/Variable/Class/Enum nodes
* keep a lean properties blob (lossless — those fields are always zero for
* non-functions). Cuts RAM, gbuf-merge copy and dump volume. Mirrors
* pass_definitions.c::build_def_props — keep both in sync. */
const bool is_fn =
def->label && (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0);
const char *test_role = def->test_role == CBM_TEST_ROLE_CASE ? ",\"test_role\":\"case\""
: def->test_role == CBM_TEST_ROLE_SUITE ? ",\"test_role\":\"suite\""
: "";
int n;
if (is_fn) {
n = snprintf(buf, bufsize,
"{\"complexity\":%d,\"cognitive\":%d,\"loop_count\":%d,\"loop_depth\":%d,"
"\"self_recursive\":%s,\"param_count\":%d,\"max_access_depth\":%d,"
"\"linear_scan_in_loop\":%d,\"alloc_in_loop\":%d,\"recursion_in_loop\":%s,"
"\"unguarded_recursion\":%s,"
"\"lines\":%d,\"is_exported\":%s,\"is_test\":%s,\"is_entry_point\":%s%s",
def->complexity, def->cognitive, def->loop_count, def->loop_depth,
def->is_recursive ? "true" : "false", def->param_count, def->max_access_depth,
def->linear_scan_in_loop, def->alloc_in_loop,
def->recursion_in_loop ? "true" : "false",
def->unguarded_recursion ? "true" : "false", def->lines,
def->is_exported ? "true" : "false", def->is_test ? "true" : "false",
def->is_entry_point ? "true" : "false", test_role);
} else {
n = snprintf(buf, bufsize,
"{\"complexity\":%d,\"lines\":%d,\"is_exported\":%s,\"is_test\":%s,"
"\"is_entry_point\":%s%s",
def->complexity, def->lines, def->is_exported ? "true" : "false",
def->is_test ? "true" : "false", def->is_entry_point ? "true" : "false",
test_role);
}
if (n <= 0 || (size_t)n >= bufsize) {
buf[0] = '\0';
return;
}
size_t pos = (size_t)n;
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
/* The buffer is sized for every field below (pp_props_buf), so none can be
* squeezed out. */
append_json_raw(buf, bufsize, &pos, "variants", def->variants);
append_json_string(buf, bufsize, &pos, "variant_key", def->variant_key);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
append_json_string(buf, bufsize, &pos, "parent_class", def->parent_class);
append_json_str_array(buf, bufsize, &pos, "decorators", def->decorators);
append_json_str_array(buf, bufsize, &pos, "base_classes", def->base_classes);
append_json_str_array(buf, bufsize, &pos, "param_names", def->param_names);
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
append_json_string(buf, bufsize, &pos, kv[0], kv[SKIP_ONE]);
}
/* MinHash fingerprint — append if present and buffer has room.
* Hex-encoded K=64 uint32 = 512 chars + key/quotes ≈ 520 chars. */
if (def->fingerprint && def->fingerprint_k > 0 &&
pos + CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD < bufsize) {
char fp_hex[CBM_MINHASH_HEX_BUF];
cbm_minhash_to_hex((const cbm_minhash_t *)def->fingerprint, fp_hex, sizeof(fp_hex));
append_json_string(buf, bufsize, &pos, "fp", fp_hex);
}
/* AST structural profile — append if present and buffer has room. */
if (def->structural_profile && pos + CBM_AST_PROFILE_BUF < bufsize) {
append_json_string(buf, bufsize, &pos, "sp", def->structural_profile);
}
/* Body tokens — raw identifiers from function body AST for semantic search. */
if (def->body_tokens && pos + CBM_SZ_512 < bufsize) {
append_json_string(buf, bufsize, &pos, "bt", def->body_tokens);
}
if (pos < bufsize - SKIP_ONE) {
buf[pos] = '}';
buf[pos + SKIP_ONE] = '\0';
}
}
/* True for languages whose module QN derives from the CONTAINING DIRECTORY
* (Java/Go package). MUST match cbm_lang_module_is_dir() (internal/cbm/helpers.c)
* and pxc_module_is_dir() (pass_lsp_cross.c) so same-module callee resolution
* keys against the directory-based def-node QNs in the registry. */
static bool pp_module_is_dir(CBMLanguage lang) {
return lang == CBM_LANG_JAVA || lang == CBM_LANG_GO;
}
static bool is_checked_exception(const char *name) {
if (!name) {
return false;
}
if (strstr(name, "Error") || strstr(name, "Panic") || strstr(name, "error") ||
strstr(name, "panic")) {
return false;
}
return true;
}
static const char *resolve_as_class(const cbm_registry_t *reg, const char *name,
const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
cbm_resolution_t res =
cbm_registry_resolve_lang(reg, name, module_qn, imp_keys, imp_vals, imp_count, lang);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
return NULL;
}
/* Accept any type-like container (Class/Struct/Interface/Enum/Type/Trait):
* base classes, Rust `impl Trait for S` struct receivers, and Go struct
* embedding all resolve through here. Struct included so the struct receiver
* of an IMPLEMENTS edge is not dropped. */
const char *label = cbm_registry_label_of(reg, res.qualified_name);
if (!cbm_label_is_type_like(label)) {
return NULL;
}
return res.qualified_name;
}
static void extract_decorator_func(const char *dec, char *out, size_t outsz) {
out[0] = '\0';
if (!dec) {
return;
}
const char *start = dec;
while (*start == '@' || *start == '#' || *start == '[' || *start == ' ') {
start++;
}
size_t len = 0;
while (start[len] && start[len] != '(' && start[len] != '[' && start[len] != ']' &&
start[len] != ' ' && start[len] != ',') {
len++;
}
if (len == 0 || len >= outsz) {
return;
}
memcpy(out, start, len);
out[len] = '\0';
}
/* ── File sort for tail-latency reduction ────────────────────────── */
typedef struct {
int idx;
int64_t size;
} file_sort_entry_t;
static int compare_by_size_desc(const void *a, const void *b) {
const file_sort_entry_t *fa = a;
const file_sort_entry_t *fb = b;
if (fb->size > fa->size) {
return SKIP_ONE;
}
if (fb->size < fa->size) {
return CBM_NOT_FOUND;
}
return 0;
}
/* ── Phase 3A: Parallel Extract ──────────────────────────────────── */
#define CBM_CACHE_LINE CBM_SZ_128
typedef struct __attribute__((aligned(CBM_CACHE_LINE))) {
cbm_gbuf_t *local_gbuf;
int nodes_created;
int errors;
char _pad[CBM_CACHE_LINE - sizeof(cbm_gbuf_t *) - (PP_ESC_SPACE * sizeof(int))];
} extract_worker_state_t;
typedef struct {
const cbm_file_info_t *files;
file_sort_entry_t *sorted;
int file_count;
const char *project_name;
const char *repo_path;
extract_worker_state_t *workers;
int max_workers;
_Atomic int next_worker_id;
CBMFileResult **result_cache;
_Atomic int64_t *shared_ids;
_Atomic int *cancelled;
_Atomic int next_file_idx;
cbm_pipeline_ctx_t *pctx; /* spill store + mode latch live here */
_Atomic uint8_t *slot_state; /* per file: 0 in progress, 1 cached, 2 parked */
_Atomic int spill_sweeps_running; /* workers inside pp_spill_sweep right now */
int spill_env; /* CBM_MEM_SPILL for this run: 1 force, 0 off, 2 budget decides */
cbm_mutex_t spill_mu; /* serializes the one store open */
_Atomic int spill_unavailable; /* store could not open: never retry */
cbm_pkg_entries_t *pkg_entries; /* per-worker manifest arrays (separate allocation) */
bool retain_sources; /* copy source into result->arena for cross-file LSP */
size_t retain_total_budget_bytes; /* project-wide retention cap (peak-RSS bound) */
size_t retain_per_file_max_bytes; /* per-file retention cap */
_Atomic int64_t retained_bytes; /* total source bytes copied into result arenas */
_Atomic int retain_cap_warned; /* WARN index.retain_capped emitted once per run */
/* Per-worker skip lists (separate allocation, indexed by worker_id — no hot-
* path lock). Merged into the pipeline in the sequential merge loop. */
pp_err_list_t *err_lists;
_Atomic int oversized_warned; /* throttle for the index.file_oversized WARN */
/* Back-pressure futility latch: set when a full collect+nap cycle ended
* still over budget — the resident floor (graph + retained sources), not
* in-flight transients, holds the memory, so napping cannot reclaim it.
* While set, pulls skip the nap (the designed soft overshoot); the cheap
* over-budget probe re-arms the gate once RSS drains under budget. */
_Atomic int bp_futile;
/* Decision A (#1997 #832): set once futility was CONFIRMED by a second
* full cycle that still ended over budget. Every pull loop breaks on it
* and extract returns CBM_PIPELINE_ABORT_OVER_BUDGET. Deliberately not
* the shared cancel token — that one belongs to client cancellation. */
_Atomic int over_budget_abort;
const CBMMacroTable *macro_table; /* ObjectScript $$$macros (NULL if none) */
const CBMReturnTypeTable *return_type_table; /* ObjectScript return types (NULL if none) */
/* Superlinearity probe — see profile.h. Ticked on each file claim below. */
cbm_scale_probe_t scale;
} extract_ctx_t;
/* Cap on the number of index.file_oversized WARN lines (the full list still goes
* to the response/logfile — this only throttles the stderr noise). */
enum { PP_OVERSIZED_WARN_MAX = 32 };
/* Upper bound of build_def_props' fixed prefix: the Function/Method metrics
* literal (about 330 B) with nine ints and seven true/false/test_role strings at
* their widest stays under 600 B. */
enum { PP_PROPS_PREFIX_MAX = CBM_SZ_1K };
/* Serialized size of one optional field as append_json_string emits it
* (0 when the field is absent). */
static size_t pp_str_field_len(const char *key, const char *val) {
return val && val[0] ? strlen(key) + pp_json_escaped_len(val) + PP_JSON_FIELD_OVERHEAD : 0;
}
/* Serialized size of a string array as append_json_str_array emits it. */
static size_t pp_arr_field_len(const char *key, const char **arr) {
if (!arr || !arr[0]) {
return 0;
}
size_t n = strlen(key) + PP_JSON_FIELD_OVERHEAD;
for (int i = 0; arr[i]; i++) {
n += pp_json_escaped_len(arr[i]) + PP_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
}
return n;
}
/* A def's properties buffer, sized so EVERY field build_def_props appends fits:
* the fixed metrics prefix (PP_PROPS_PREFIX_MAX) plus each optional field's
* whole serialized form. A field that does not fit is dropped whole, so a buffer
* sized for only some of them let a long signature, decorator list or parameter
* list squeeze out the MinHash fingerprint ("fp"), structural profile ("sp") and
* body tokens ("bt") behind them: no SIMILAR_TO edge and fewer
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
* ImageController actions, 2026-10-18). fp/sp/bt are appended behind their own
* room checks, so both the check's reserve and the field are counted. Returns
* `stack` when everything fits it, or when the larger buffer cannot be
* allocated. Twin of pass_definitions.c -- keep both in sync. */
static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
size_t need = PP_PROPS_PREFIX_MAX + pp_str_field_len("docstring", def->docstring);
if (def->variants && def->variants[0]) {
need += strlen("variants") + strlen(def->variants) + PP_JSON_FIELD_OVERHEAD;
}
need += pp_str_field_len("variant_key", def->variant_key) +
pp_str_field_len("signature", def->signature) +
pp_str_field_len("return_type", def->return_type) +
pp_str_field_len("parent_class", def->parent_class) +
pp_arr_field_len("decorators", def->decorators) +
pp_arr_field_len("base_classes", def->base_classes) +
pp_arr_field_len("param_names", def->param_names) +
pp_arr_field_len("param_types", def->param_types) +
pp_str_field_len("route_path", def->route_path) +
pp_str_field_len("route_method", def->route_method) +
pp_str_field_len("http_client", def->http_client) +
pp_str_field_len("http_base_url", def->http_base_url);
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
need += pp_str_field_len(kv[0], kv[SKIP_ONE]);
}
if (def->fingerprint && def->fingerprint_k > 0) {
need += CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD + SKIP_ONE;
}
if (def->structural_profile) {
need += CBM_AST_PROFILE_BUF + pp_str_field_len("sp", def->structural_profile) + SKIP_ONE;
}
if (def->body_tokens) {
need += CBM_SZ_512 + pp_str_field_len("bt", def->body_tokens) + SKIP_ONE;
}
need += PP_ESC_SPACE; /* the closing '}' + NUL */
if (need <= *size) {
return stack;
}
char *buf = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, need);
if (!buf) {
return stack;
}
*size = need;
return buf;
}
/* Insert one definition node (and its route if present) into the local gbuf. */
static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_stem_marks_t *marks,
const cbm_file_info_t *fi, CBMDefinition *def) {
char stack[CBM_SZ_2K];
size_t props_size = sizeof(stack);
char *props = pp_props_buf(def, stack, &props_size);
build_def_props(props, props_size, def);
const char *rel = def->file_path ? def->file_path : fi->rel_path;
char marked[CBM_SZ_4K];
const char *qn = def->label && strcmp(def->label, "Module") == 0
? cbm_stem_marks_module_qn(marks, def->qualified_name, rel, fi->language,
marked, sizeof(marked))
: def->qualified_name;
int64_t func_id =
cbm_gbuf_upsert_node(ws->local_gbuf, def->label ? def->label : "Function", def->name, qn,
rel, (int)def->start_line, (int)def->end_line, props);
if (props != stack) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, props);
}
ws->nodes_created++;
cbm_pipeline_emit_def_route(ws->local_gbuf, func_id, def, fi->rel_path);
}
static void log_extract_fail(int pos, uint64_t ms, const char *path) {
if (pos < PP_LOG_THRESH) {
cbm_log_warn("parallel.extract.file.fail", "pos", itoa_log(pos), "elapsed_ms",
itoa_log((int)ms), "path", path);
}
}
static void log_extract_done(int pos, uint64_t ms, int defs, const char *path) {
if (pos < PP_LOG_THRESH || ms > PP_TIMER_THRESH) {
cbm_log_info("parallel.extract.file.done", "pos", itoa_log(pos), "elapsed_ms",
itoa_log((int)ms), "defs", itoa_log(defs), "path", path);
}
}
/* One back-pressure cycle: reclaim this thread's freed pages, then nap in
* bounded 3 ms steps while the process stays over budget. Counted for test
* observability. Returns true when the FULL cycle elapsed still over budget —
* the resident floor, not in-flight transients, holds the memory. */
static bool pp_backpressure_cycle_still_over(extract_ctx_t *ec) {
cbm_mem_collect();
atomic_fetch_add_explicit(&g_bp_nap_cycles, SKIP_ONE, memory_order_relaxed);
int bp = 0;
for (; bp < PP_BACKPRESSURE_MAX_SPINS && cbm_mem_over_budget() &&
!atomic_load_explicit(ec->cancelled, memory_order_relaxed) &&
!atomic_load_explicit(&ec->over_budget_abort, memory_order_relaxed);
bp++) {
struct timespec nap = {0, PP_BACKPRESSURE_NAP_NS};
cbm_nanosleep(&nap, NULL);
}
return bp == PP_BACKPRESSURE_MAX_SPINS && cbm_mem_over_budget();
}
/* Decision A (#1997 #832): back-pressure was futile and the confirmation
* cycle still ended over budget. The budget stops being advisory here: flag
* the attempt so every worker stops pulling and extract returns
* CBM_PIPELINE_ABORT_OVER_BUDGET. Cooperative on purpose — the failure must
* travel as a clean-exit worker response, never a kill, or the supervisor
* would quarantine innocent files. */
static void pp_fail_whole_over_budget(extract_ctx_t *ec) {
if (atomic_exchange_explicit(&ec->over_budget_abort, 1, memory_order_relaxed) == 0) {
cbm_log_error("mem.budget.exceeded", "rss_mb",
itoa_log((int)(cbm_mem_rss() / (1024 * 1024))), "budget_mb",
itoa_log((int)(cbm_mem_budget() / (1024 * 1024))), "phase",
"parallel_extract", "action", "fail_whole");
}
}
/* ── Spill / admission control ──────────────────────────────────────── */
/* Spill mode is entered this fraction short of the budget: 1/16 = 6.25%,
* ~940 MB at 15 GB, above the 4% the kernel's in-flight files were measured
* to carry past the line. */
enum { PP_SPILL_EARLY_DIV = 16 };
/* CBM_MEM_SPILL=1 forces spill mode from the first file (tests, small
* machines); CBM_MEM_SPILL=0 keeps results in memory even over budget. Read
* once per extraction run, never cached per process: a test toggles it. */
static int pp_spill_env_read(void) {
char buf[CBM_SZ_16];
if (cbm_safe_getenv("CBM_MEM_SPILL", buf, sizeof(buf), NULL)) {
return buf[0] == '1' ? 1 : 0;
}
return 2; /* unset: the budget decides */
}
static bool pp_spill_forced_by_env(const extract_ctx_t *ec) {
return ec->spill_env == 1;
}
static bool pp_spill_allowed(const extract_ctx_t *ec) {
return ec->spill_env != 0;
}
/* Latch spill mode (once) and open the store. The store is open BEFORE the
* latch is visible: a peer that reads the latch must also find the store, or
* it sees "nothing to park" while this worker is still opening files and runs
* the futility cycle instead of joining the sweep (kernel, 16 GB budget,
* 2026-09-13: the abort fired before the first sweep had logged). Never fails
* the run: without a store the results simply stay in memory and the nap /
* futility path applies as before. */
static void pp_spill_enter(extract_ctx_t *ec, const char *reason) {
if (!ec->pctx || !ec->pctx->spill_allowed || !pp_spill_allowed(ec)) {
return;
}
if (atomic_load_explicit(&ec->pctx->spill_mode, memory_order_acquire) != 0 ||
atomic_load_explicit(&ec->spill_unavailable, memory_order_relaxed) != 0) {
return;
}
cbm_mutex_lock(&ec->spill_mu);
if (atomic_load_explicit(&ec->pctx->spill_mode, memory_order_acquire) == 0 &&
atomic_load_explicit(&ec->spill_unavailable, memory_order_relaxed) == 0) {
if (!ec->pctx->spill) {
ec->pctx->spill =
cbm_result_spill_open(cbm_resolve_cache_dir(), ec->max_workers, ec->file_count);
}
size_t mb = (size_t)1024 * 1024;
cbm_log_warn("mem.spill.on", "reason", reason, "charged_mb",
itoa_log((int)(cbm_mem_charged() / mb)), "budget_mb",
itoa_log((int)(cbm_mem_budget() / mb)), "store",
ec->pctx->spill ? "open" : "unavailable");
if (ec->pctx->spill) {
atomic_store_explicit(&ec->pctx->spill_mode, 1, memory_order_release);
} else {
atomic_store_explicit(&ec->spill_unavailable, 1, memory_order_relaxed);
}
}
cbm_mutex_unlock(&ec->spill_mu);
}
static bool pp_spill_active(const extract_ctx_t *ec) {
/* The atomic gates the pointer, not the other way round. pp_spill_enter
* assigns ec->pctx->spill under spill_mu and only THEN release-stores
* spill_mode, so a reader that has acquired a non-zero spill_mode is
* guaranteed to see the finished pointer. Testing the pointer first read it
* with no synchronisation at all while another worker was publishing it —
* a genuine data race on an 8-byte write, which TSan caught at
* pass_parallel.c:817 against this line. Short-circuit order is load
* bearing here; do not reorder these terms. */
return ec->pctx && atomic_load_explicit(&ec->pctx->spill_mode, memory_order_acquire) != 0 &&
ec->pctx->spill;
}
CBMFileResult *cbm_pipeline_result_acquire(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
int i, cbm_result_want_fn want, bool *loaded) {
*loaded = false;
if (cache && cache[i]) {
return (!want || want(cache[i])) ? cache[i] : NULL;
}
if (!ctx || !ctx->spill || !cbm_result_spill_has(ctx->spill, i)) {
return NULL;
}
if (want) {
CBMFileResult hdr;
if (!cbm_result_spill_peek_header(ctx->spill, i, &hdr) || !want(&hdr)) {
return NULL;
}
}
CBMFileResult *r = cbm_result_spill_load(ctx->spill, i);
*loaded = r != NULL;
return r;
}
void cbm_pipeline_result_release(CBMFileResult *r, bool loaded) {
if (r && loaded) {
cbm_free_result(r);
}
}
void cbm_pipeline_spill_close(cbm_pipeline_ctx_t *ctx) {
if (!ctx || !ctx->spill) {
return;
}
int64_t parked = 0;
int64_t bytes = 0;
int64_t loads = 0;
cbm_result_spill_stats(ctx->spill, &parked, &bytes, &loads);
cbm_log_info("mem.spill.done", "parked", itoa_log((int)parked), "mb",
itoa_log((int)(bytes / (1024 * 1024))), "loads", itoa_log((int)loads));
cbm_result_spill_close(ctx->spill);
ctx->spill = NULL;
atomic_store_explicit(&ctx->spill_mode, 0, memory_order_release);
cbm_work_arena_release();
}
/* Park every cached result that is complete. Slots are claimed 1 -> 2 so two
* workers never park the same result; a failed park hands the slot back. */
/* True while a cached (not yet parked) result exists or a sweep is running:
* memory can still come down without anyone napping. */
static bool pp_spill_work_remains(const extract_ctx_t *ec) {
if (!ec->slot_state) {
return false;
}
if (atomic_load_explicit(&ec->spill_sweeps_running, memory_order_acquire) > 0) {
return true;
}
for (int i = 0; i < ec->file_count; i++) {
if (atomic_load_explicit(&ec->slot_state[i], memory_order_relaxed) == 1) {
return true;
}
}
return false;
}
static int pp_spill_sweep(extract_ctx_t *ec, int worker_id) {
if (!pp_spill_active(ec) || !ec->slot_state) {
return 0;
}
atomic_fetch_add_explicit(&ec->spill_sweeps_running, 1, memory_order_acq_rel);
int parked = 0;
for (int i = 0; i < ec->file_count; i++) {
if (atomic_load_explicit(&ec->over_budget_abort, memory_order_relaxed)) {
break;
}
uint8_t expected = 1;
if (!atomic_compare_exchange_strong_explicit(&ec->slot_state[i], &expected, (uint8_t)2,
memory_order_acq_rel, memory_order_relaxed)) {
continue;
}
CBMFileResult *r = ec->result_cache[i];
if (r && cbm_result_spill_park(ec->pctx->spill, worker_id, i, r)) {
ec->result_cache[i] = NULL;
parked++;
if ((parked & 255) == 0) {
cbm_mem_release_to_os();
}
} else {
atomic_store_explicit(&ec->slot_state[i], (uint8_t)1, memory_order_release);
}
}
if (parked > 0) {
cbm_mem_release_to_os();
cbm_log_info("mem.spill.sweep", "parked", itoa_log(parked), "charged_mb",
itoa_log((int)(cbm_mem_charged() / ((size_t)1024 * 1024))));
}
atomic_fetch_sub_explicit(&ec->spill_sweeps_running, 1, memory_order_acq_rel);
return parked;
}
/* ── Post-extraction projection (#2184) ───────────────────────────────
* Spill is entered DURING extraction, when the charge crosses the latch below.
* The phases after it -- registry build, cross-LSP prepare (all_defs, the
* per-language registries, surface rows, the module index) and resolve (edges
* into the graph buffer, cross-LSP appends into the cached results) -- cannot
* spill, and they grow the charge by a sizeable fraction of what extraction
* left. A run that ended extraction just under the latch therefore kept every
* result in memory and went over the budget later: openclaw, 8 workers,
* 4079 MB budget: 3678 MB charged at extraction end, 5829 MB in resolve.
*
* So extraction end projects that growth from the result counts and spills
* before handing over when charged + growth would cross the latch. A pure
* function of counts (O9): the same repo decides the same way on every run.
* Per-unit costs fitted 2026-09-25 (M5 Pro, release build, 8 workers) on the
* charge growth from the parallel_extract mark to the parallel_resolve mark:
*
* corpus files defs calls+usages growth model
* go 21882 735895 5,504,282 1307 MB 1909 MB
* django 4169 75419 591,350 231 MB 215 MB
* kotlin 5247 49669 421,788 129 MB 166 MB
* rust 818 23717 265,957 79 MB 76 MB
* php 2435 14691 137,969 47 MB 58 MB
* openclaw 48203 748829 >= 7,402,832 2151 MB >= 2419 MB
*
* It never under-reads by more than 7% (inside the budget/16 margin) and
* over-reads Go by 46% -- the safe side: an unneeded spill costs disk reads,
* never graph content (spilled and in-memory runs build the same graph). */
enum {
PP_POST_BYTES_PER_DEF = 1280, /* registry entry + LSP def + cross registries */
PP_POST_BYTES_PER_REF = 160, /* per call / usage: resolved edge + appends */
PP_POST_BYTES_PER_FILE = 8192, /* per-file tables: modules, surfaces, imports */
};
static size_t pp_post_extract_growth(const extract_ctx_t *ec, int64_t *defs_out,
int64_t *refs_out) {
int64_t defs = 0;
int64_t refs = 0;
for (int i = 0; i < ec->file_count; i++) {
const CBMFileResult *r = ec->result_cache[i];
if (r) {
defs += r->defs.count;
refs += (int64_t)r->calls.count + r->usages.count;
}
}
*defs_out = defs;
*refs_out = refs;
return (size_t)defs * PP_POST_BYTES_PER_DEF + (size_t)refs * PP_POST_BYTES_PER_REF +
(size_t)ec->file_count * PP_POST_BYTES_PER_FILE;
}
/* Enter spill mode at extraction end when the projected post-extraction growth
* would carry the charge over the latch; the final sweep then parks every
* cached result before registry build. No-op when spill is already on, not
* allowed for this owner, or no budget is set. */
static void pp_spill_if_projected_over(extract_ctx_t *ec) {
size_t budget = cbm_mem_budget();
if (budget == 0 || !ec->pctx || !ec->pctx->spill_allowed || !pp_spill_allowed(ec) ||
pp_spill_active(ec) || atomic_load_explicit(&ec->over_budget_abort, memory_order_relaxed)) {
return;
}
int64_t defs = 0;
int64_t refs = 0;
size_t growth = pp_post_extract_growth(ec, &defs, &refs);
size_t charged = cbm_mem_charged();
size_t line = budget - budget / PP_SPILL_EARLY_DIV;
bool spill = growth > line || charged > line - growth;
const size_t mb = (size_t)1024 * 1024;
char v[6][CBM_SZ_32];
snprintf(v[0], sizeof(v[0]), "%zu", charged / mb);
snprintf(v[1], sizeof(v[1]), "%zu", growth / mb);
snprintf(v[2], sizeof(v[2]), "%zu", line / mb);
snprintf(v[3], sizeof(v[3]), "%lld", (long long)defs);
snprintf(v[4], sizeof(v[4]), "%lld", (long long)refs);
snprintf(v[5], sizeof(v[5]), "%d", ec->file_count);
cbm_log_info("mem.post_extract.projection", "charged_mb", v[0], "growth_mb", v[1], "line_mb",
v[2], "defs", v[3], "refs", v[4], "files", v[5], "decision",
spill ? "spill" : "keep");
if (spill) {
pp_spill_enter(ec, "post_extract_projection");
}
}
/* Diagnostic (CBM_MEM_PHASES=1): where does the charge go between the
* near-budget latch and the first over-budget observation? One line per
* 256 MB step of the charge above its last logged value while spill mode is
* on, and one at the first over-budget observation, each with the
* footprint / commit / tracked breakdown and the class table. */
static _Atomic size_t g_probe_last_mb = 0;
static _Atomic int g_probe_over_logged = 0;
static void pp_charge_probe(extract_ctx_t *ec, bool over) {
if (!cbm_mem_phases_enabled() || !pp_spill_active(ec)) {
return;
}
enum { PROBE_STEP_MB = 256, PROBE_MB = 1024 * 1024 };
size_t charged_mb = cbm_mem_charged() / PROBE_MB;
size_t last = atomic_load_explicit(&g_probe_last_mb, memory_order_relaxed);
bool step = charged_mb >= last + PROBE_STEP_MB &&
atomic_compare_exchange_strong_explicit(&g_probe_last_mb, &last, charged_mb,
memory_order_relaxed, memory_order_relaxed);
bool first_over =
over && atomic_exchange_explicit(&g_probe_over_logged, 1, memory_order_relaxed) == 0;
if (!step && !first_over) {
return;
}
cbm_log_info("mem.charge.probe", "event", first_over ? "first_over" : "step", "charged_mb",
itoa_log((int)charged_mb), "footprint_mb",
itoa_log((int)(cbm_mem_footprint() / PROBE_MB)), "commit_mb",
itoa_log((int)(cbm_mem_allocator_committed() / PROBE_MB)), "tracked_mb",
itoa_log((int)(cbm_mem_tracked_live_bytes() / PROBE_MB)));
cbm_mem_class_log(first_over ? "charge.first_over" : "charge.step");
}
static void extract_worker(int worker_id, void *ctx_ptr) {
extract_ctx_t *ec = ctx_ptr;
extract_worker_state_t *ws = &ec->workers[worker_id];
/* Lazy gbuf creation */
if (!ws->local_gbuf) {
ws->local_gbuf = cbm_gbuf_new_worker(ec->project_name, ec->repo_path, ec->shared_ids);
}
/* Pull files from shared atomic counter */
while (SKIP_ONE) {
int sort_pos =
atomic_fetch_add_explicit(&ec->next_file_idx, SKIP_ONE, memory_order_relaxed);
if (sort_pos >= ec->file_count) {
break;
}
cbm_scale_tick(&ec->scale, sort_pos);
if (atomic_load_explicit(ec->cancelled, memory_order_relaxed) ||
atomic_load_explicit(&ec->over_budget_abort, memory_order_relaxed)) {
break;
}
/* Memory back-pressure (large repos): if the process is over its RSS
* budget, reclaim this thread's freed pages and nap so peer workers can
* finish their current file and return memory before this worker adds
* another parse working set. Caps the concurrent extraction transient
* near the budget instead of letting all workers parse their biggest
* files at once. Self-disabling when the budget is unset (tests) or RSS
* is under budget; bounded spins avoid deadlock when the resident graph
* is itself near budget.
*
* Futility latch: when a FULL nap cycle ends still over budget, the
* resident floor — not transients — holds the memory; napping again on
* the next pull cannot reclaim it and only idles workers (linux kernel:
* one full cycle per pull ≈ 390 s at 79% avg CPU). The worker that
* latches bp_futile (0→1) runs ONE confirmation cycle while its peers
* skip the nap. A confirmation that ends under budget re-arms the gate
* (RSS drained after all); one that still ends over budget fails the
* attempt whole (decision A, #1997 #832): every worker stops pulling,
* extract returns CBM_PIPELINE_ABORT_OVER_BUDGET, nothing is published
* and the previously serving index keeps answering. */
if (cbm_mem_budget() > 0) {
bool over = cbm_mem_over_budget();
pp_charge_probe(ec, over);
/* Anticipation: the gate sees the crossing per file pull, and the
* workers' in-flight files carry the charge past the line before
* the first sweep lands (kernel, 15 GB budget: high-water 15.65
* GB, 4% over, all of it set in that window). Spill mode is
* entered PP_SPILL_EARLY_DIV-th short of the budget, so results
* stop accumulating in memory while the headroom still covers the
* in-flight work. The nap / futility path still keys on the
* budget itself. */
if (!over && !pp_spill_active(ec)) {
size_t budget = cbm_mem_budget();
if (cbm_mem_charged() > budget - budget / PP_SPILL_EARLY_DIV) {
pp_spill_enter(ec, "near_budget");
}
}
/* The MACHINE can be out of memory while our own charge sits well
* under the budget: measured 2026-09-18 on a 48 GB host, 22 GB
* charged against a 24 GB budget, and the OS killed the worker
* anyway because a VM held the rest. Spill on real scarcity too.
* RELIEF ONLY — `over` is deliberately NOT set from this, so the
* futility/abort path below stays keyed to OUR budget. Another
* process's allocation spike must never fail this run.
* The cheap charge comparison guards the syscall, so the pressure
* query costs nothing until we are already in the danger zone. */
if (!pp_spill_active(ec) && cbm_mem_charged() > cbm_mem_budget() / 2 &&
cbm_mem_system_under_pressure()) {
pp_spill_enter(ec, "system_pressure");
(void)pp_spill_sweep(ec, worker_id);
}
bool settling = false;
if (over) {
/* Admission control, first response: park what can be parked.
* Only what is still over budget after that -- the floor --
* reaches the nap / futility / abort path below, and only
* once nothing is left to park anywhere. */
pp_spill_enter(ec, "over_budget");
int parked = pp_spill_sweep(ec, worker_id);
over = cbm_mem_over_budget();
if (over && pp_spill_active(ec) && (parked > 0 || pp_spill_work_remains(ec))) {
over = false; /* memory is still on its way down */
settling = true;
}
}
bool futile = atomic_load_explicit(&ec->bp_futile, memory_order_relaxed) != 0;
if (over && !futile) {
/* Act only on the 0→1 transition: all workers race into the
* gate before anyone latches, so a plain store would confirm
* (and WARN) once per worker. */
if (pp_backpressure_cycle_still_over(ec) &&
atomic_exchange_explicit(&ec->bp_futile, 1, memory_order_relaxed) == 0) {
cbm_log_warn("mem.backpressure.futile", "action", "confirm");
if (pp_backpressure_cycle_still_over(ec)) {
pp_fail_whole_over_budget(ec);
} else {
atomic_store_explicit(&ec->bp_futile, 0, memory_order_relaxed);
}
}
} else if (!over && !settling && futile) {
/* Re-arm only on a genuine under-budget reading. A reading the
* spill shortcut produced ("still on its way down") is not
* one: re-arming on it made the next over-budget pull pay a
* full nap cycle again -- the gate re-paid per pull that
* pipeline_backpressure_futile_nap_disengages guards against
* (TSan lane on PR #2202: 8 cycles against a bound of 7). */
atomic_store_explicit(&ec->bp_futile, 0, memory_order_relaxed);
}
}
if (atomic_load_explicit(&ec->over_budget_abort, memory_order_relaxed)) {
break;
}
int file_idx = ec->sorted[sort_pos].idx;
const cbm_file_info_t *fi = &ec->files[file_idx];
pp_err_list_t *errs = ec->err_lists ? &ec->err_lists[worker_id] : NULL;
/* Crash-quarantine skip (Stage 3c): a file the supervisor pinned as a
* crasher must never be extracted again. Record it as a phase="crash"
* skip in this worker's list (merged into the pipeline's file-error list
* later, surfacing in skipped[]) and move on — the good files still
* index and status stays "indexed". No-op unless the supervisor set
* CBM_INDEX_QUARANTINE_FILE. Covers the parallel path; the supervisor's
* single-threaded recovery run instead takes the sequential path
* (pass_definitions.c), and cbm_extract_file's hard guard backstops both. */
if (cbm_index_is_quarantined(fi->rel_path)) {
const char *phase = cbm_index_quarantine_phase(fi->rel_path);
if (!phase) {
phase = "crash";
}
const char *reason = (strcmp(phase, "hang") == 0) ? "quarantined after hang"
: (strcmp(phase, "error") == 0) ? "quarantined after error"
: "quarantined after crash";
pp_err_add(errs, fi->rel_path, reason, phase);
ws->errors++;
(void)cbm_pipeline_test_extraction_ok(ec->pctx, fi->language, NULL);
continue;
}
/* Read + extract */
int source_len = 0;
long file_size = 0;
cbm_read_status_t rst = CBM_READ_OK;
char *source = read_file(fi->path, &source_len, &file_size, &rst);
if (!source) {
ws->errors++;
if (rst == CBM_READ_OVERSIZED) {
/* Never a silent drop: record the oversized skip + a throttled
* WARN so the file surfaces in the response/logfile. */
long cap = cbm_max_file_bytes();
char reason[96];
snprintf(reason, sizeof(reason), "oversized (%lld MB > %lld MB)",
(long long)(file_size / (CBM_SZ_1K * CBM_SZ_1K)),
(long long)(cap / (CBM_SZ_1K * CBM_SZ_1K)));
pp_err_add(errs, fi->rel_path, reason, "oversized");
if (atomic_fetch_add_explicit(&ec->oversized_warned, SKIP_ONE,
memory_order_relaxed) < PP_OVERSIZED_WARN_MAX) {
cbm_log_warn("index.file_oversized", "path", fi->rel_path, "size_mb",
itoa_log((int)(file_size / (CBM_SZ_1K * CBM_SZ_1K))), "cap_mb",
itoa_log((int)(cap / (CBM_SZ_1K * CBM_SZ_1K))));
}
} else if (rst == CBM_READ_OPEN_FAIL || rst == CBM_READ_OOM) {
pp_err_add(errs, fi->rel_path, "read failed", "read");
}
if (rst != CBM_READ_EMPTY) {
(void)cbm_pipeline_test_extraction_ok(ec->pctx, fi->language, NULL);
}
continue;
}
/* Per-file start log: shows which file each worker is processing.
* Critical for diagnosing stuck workers on large vendored files.
*
* Under a crash-durable log (i.e. a supervised worker) EVERY file gets
* its line, not just the first rounds. That log is the only evidence a
* contained crash or a kill leaves behind, and #1145/#1130 are
* unattributable precisely because it never named the file that was in
* flight — the run ends with the culprit still on the last lines. One
* line per file, never per node. */
if (sort_pos < PP_LOG_THRESH || /* first 2 rounds of workers = most interesting */
cbm_log_crash_durable()) {
cbm_log_info("parallel.extract.file.start", "pos", itoa_log(sort_pos), "size_kb",
itoa_log(source_len / CBM_SZ_1K), "path", fi->rel_path);
}
uint64_t file_t0 = extract_now_ns();
/* Export XML uses the same cache slot as every physical file, so its
* generated classes are composed before entering the common registry
* and resolution lifecycle. */
CBMFileResult *result =
cbm_pipeline_test_force_extract_null(ec->pctx, fi->language) ? NULL
: fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT
? cbm_pipeline_extract_objectscript_export(source, source_len, ec->project_name,
fi->rel_path, ec->macro_table,
ec->return_type_table)
: cbm_extract_file_ex_with_tests(source, source_len, fi->language, ec->project_name,
fi->rel_path, CBM_EXTRACT_BUDGET, NULL, NULL,
ec->macro_table, ec->return_type_table,
ec->pctx->test_declarations);
uint64_t file_elapsed_ms = (extract_now_ns() - file_t0) / PP_USEC_PER_MS;
if (!cbm_pipeline_test_extraction_ok(ec->pctx, fi->language, result)) {
cbm_free_result(result);
free_source(source);
cbm_destroy_thread_parser();
cbm_slab_reclaim();
cbm_mem_collect();
continue;
}
if (!result) {
log_extract_fail(sort_pos, file_elapsed_ms, fi->rel_path);
free_source(source);
ws->errors++;
pp_err_add(errs, fi->rel_path, "extract failed", "extract");
continue;
}
if (result->has_test_definition_owners && result->lsp_skipped) {
cbm_pipeline_test_owner_error(result);
}
if (!cbm_pipeline_test_result_ok(ec->pctx, result)) {
cbm_free_result(result);
free_source(source);
cbm_destroy_thread_parser();
cbm_slab_reclaim();
cbm_mem_collect();
continue;
}
log_extract_done(sort_pos, file_elapsed_ms, result->defs.count, fi->rel_path);
/* Consume the previously-ignored has_error flag: a parse timeout / parse
* failure / unsupported-grammar result carries no defs but must still be
* reported (phase "extract", reason = the extractor's message). The empty
* result flows through unchanged below (the defs loop is a no-op). */
if (result->has_error) {
pp_err_add(errs, fi->rel_path, result->error_msg ? result->error_msg : "extract failed",
"extract");
ws->errors++;
} else if (result->parse_incomplete) {
/* Best-effort parse-coverage signal (#963): the file WAS indexed,
* but its tree contains ERROR/MISSING regions whose constructs are
* silently absent from the graph. Neither phase is a skip — both
* are recorded separately from skipped[] by the MCP layer.
* "parse_unusable" means one range covers so much of the file that
* naming the lines helps nobody; see parse_unusable in cbm.h. */
pp_err_add(errs, fi->rel_path, result->error_ranges ? result->error_ranges : "unknown",
result->parse_unusable ? "parse_unusable" : "parse_partial");
}
if (result->test_declarations_degraded) {
/* Degraded per file, not the index; see pass_definitions.c. */
pp_err_add(errs, fi->rel_path,
cbm_test_extract_status_message(result->test_declarations_degraded_status),
"test_declarations");
}
/* A truncated walk is a coverage gap like a partial parse, and until now
* it was the only one we kept to ourselves: result->walk_truncated was
* set and never read by anything, so a file the walk abandoned halfway
* was reported as fully indexed. Say how far it got — "walked 812k of
* 3.4M nodes" is the difference between a graph with a known hole and a
* graph that quietly lies about its coverage. Independent of the
* branches above: a truncated walk is not a parse error. */
if (result->walk_truncated) {
char how_far[CBM_SZ_64];
snprintf(how_far, sizeof(how_far), "%u/%u nodes walked", result->walk_nodes_visited,
result->tree_nodes);
pp_err_add(errs, fi->rel_path, how_far, "walk_truncated");
} else if (result->lsp_skipped && !result->has_error) {
/* has_error already recorded its precise cause above. Indexed, but without the per-file
* and cross-file LSP refinement: the same kind of hole from the other direction.
* Nothing in production sets this any more except a truncated walk (handled above) and
* the test seam — it is reported anyway, so that if something sets it again the gap
* arrives named, not silent. */
char size_text[CBM_SZ_64];
snprintf(size_text, sizeof(size_text), "%u nodes", result->tree_nodes);
pp_err_add(errs, fi->rel_path, size_text, "lsp_skipped");
}
/* Create definition nodes in local gbuf */
for (int d = 0; d < result->defs.count; d++) {
CBMDefinition *def = &result->defs.items[d];
if (def->qualified_name && def->name) {
insert_def_into_gbuf(ws, ec->pctx ? ec->pctx->stem_marks : NULL, fi, def);
}
}
/* Free TSTree immediately — arena strings survive for registry+resolve.
* This makes slab reset safe: tree-sitter's internal nodes (in slab)
* are released before the slab is bulk-reclaimed. */
cbm_free_tree(result);
/* Detect and parse manifest files for package map */
{
const char *bn = strrchr(fi->rel_path, '/');
cbm_pkgmap_try_parse(bn ? bn + SKIP_ONE : fi->rel_path, fi->rel_path, source,
source_len, &ec->pkg_entries[worker_id]);
}
/* Retain source bytes in result->arena so the fused cross-file LSP
* step in resolve_worker can re-parse without re-reading from disk.
* Bounded per-file (retain_per_file_max_bytes) and by a project-wide
* budget (retain_total_budget_bytes) to bound peak RSS — see the
* retention-cap comment at the top of this file. A file DROPPED here
* is NOT lost to cross-file resolution: resolve_worker re-reads it on
* demand (bounded, freed immediately). WARN once per run so the
* operator knows retention was capped (index.retain_capped). */
if (ec->retain_sources && source_len > 0) {
bool dropped = false;
if ((size_t)source_len > ec->retain_per_file_max_bytes) {
dropped = true; /* over the per-file cap */
} else {
int64_t prior = atomic_fetch_add_explicit(&ec->retained_bytes, (int64_t)source_len,
memory_order_relaxed);
if ((size_t)(prior + (int64_t)source_len) <= ec->retain_total_budget_bytes) {
char *copy = (char *)cbm_arena_alloc(&result->arena, (size_t)source_len + 1);
if (copy) {
memcpy(copy, source, (size_t)source_len);
copy[source_len] = '\0';
result->source = copy;
result->source_len = source_len;
} else {
/* Arena OOM — not a cap drop; re-read still covers
* cross-file resolution, so don't emit the WARN. */
atomic_fetch_sub_explicit(&ec->retained_bytes, (int64_t)source_len,
memory_order_relaxed);
}
} else {
atomic_fetch_sub_explicit(&ec->retained_bytes, (int64_t)source_len,
memory_order_relaxed);
dropped = true; /* project-wide budget exhausted */
}
}
if (dropped &&
atomic_exchange_explicit(&ec->retain_cap_warned, 1, memory_order_relaxed) == 0) {
cbm_log_warn("index.retain_capped", "path", fi->rel_path ? fi->rel_path : "?",
"bytes", itoa_log((int)source_len));
}
}
/* Free source buffer — extraction captured everything needed,
* and the retention copy (if any) lives in result->arena. */
free_source(source);
/* Everything this file will ever contribute has been written; drop
* the working arena (node-text copies, abandoned array generations)
* and keep only the reachable result. See cbm_result_compact. */
cbm_result_compact(result);
/* Cache result (arena + extracted data, no tree) for Phase 3B and
* Phase 4 -- or park it straight to disk in spill mode. */
if (pp_spill_active(ec) &&
cbm_result_spill_park(ec->pctx->spill, worker_id, file_idx, result)) {
if (ec->slot_state) {
atomic_store_explicit(&ec->slot_state[file_idx], (uint8_t)2, memory_order_release);
}
} else {
ec->result_cache[file_idx] = result;
if (ec->slot_state) {
atomic_store_explicit(&ec->slot_state[file_idx], (uint8_t)1, memory_order_release);
}
}
/* Progress logging: log every 10 files (atomic read, no contention) */
if ((sort_pos + SKIP_ONE) % PP_LOG_INTERVAL == 0 || sort_pos + SKIP_ONE == ec->file_count) {
cbm_log_info("parallel.extract.progress", "done", itoa_log(sort_pos + SKIP_ONE),
"total", itoa_log(ec->file_count));
}
/* Reclaim all slab + tier2 memory between files.
*
* After cbm_free_tree(result), all tree nodes are on free lists.
* We then destroy the parser (frees its internal allocations too),
* leaving ZERO live slab/tier2 pointers. At that point, we can
* safely munmap/free every page, bounding peak memory per-file
* instead of accumulating across all 644 files.
*
* get_thread_parser() in cbm_extract_file will create a fresh
* parser for the next file — cost is microseconds vs seconds
* for parsing. This prevents unbounded memory accumulation and works
* identically on macOS, Linux, and Windows. */
cbm_destroy_thread_parser();
cbm_slab_reclaim();
cbm_mem_collect();
}
/* Final cleanup (parser already destroyed in loop, just slab state) */
cbm_work_arena_release(); /* the working arena kept between files */
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,
int worker_count) {
if (!pkg_entries) {
return;
}
/* Supplement with a repo-wide filesystem walk so manifests filtered
* by the main discoverer (package.json, composer.json — in
* IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's
* array so the existing merge below sees them. */
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0], ctx->excluded_dirs, ctx->excluded_count);
cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name));
for (int i = 0; i < worker_count; i++) {
cbm_pkg_entries_free(&pkg_entries[i]);
}
free(pkg_entries);
}
static void log_extract_mem_stats(int worker_count) {
if (cbm_mem_budget() > 0) {
size_t mb = (size_t)CBM_SZ_1K * CBM_SZ_1K;
cbm_log_info("parallel.extract.mem", "rss_mb", itoa_log((int)(cbm_mem_rss() / mb)),
"peak_mb", itoa_log((int)(cbm_mem_peak_rss() / mb)), "budget_mb",
itoa_log((int)(cbm_mem_budget() / mb)), "per_worker_mb",
itoa_log((int)(cbm_mem_worker_budget(worker_count) / mb)));
}
}
/* Forward declaration: macro table builder lives in pipeline.c (shared path). */
CBMMacroTable *cbm_build_macro_table_from_files(const cbm_file_info_t *files, int count,
const char *repo_path);
int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count,
CBMFileResult **result_cache, _Atomic int64_t *shared_ids,
int worker_count, const cbm_parallel_extract_opts_t *opts) {
cbm_parallel_extract_opts_t resolved_opts = cbm_parallel_extract_resolve_opts(opts);
if (file_count == 0) {
return 0;
}
cbm_log_info("parallel.extract.start", "files", itoa_log(file_count), "workers",
itoa_log(worker_count));
{
size_t mb = (size_t)CBM_SZ_1K * CBM_SZ_1K;
cbm_log_info("parallel.extract.retention", "retain_sources",
resolved_opts.retain_sources ? "true" : "false", "total_mb",
itoa_log((int)(resolved_opts.retain_total_budget_bytes / mb)), "per_file_mb",
itoa_log((int)(resolved_opts.retain_per_file_max_bytes / mb)));
}
/* Log per-worker memory budget */
if (cbm_mem_budget() > 0) {
size_t worker_budget = cbm_mem_worker_budget(worker_count);
cbm_log_info("parallel.mem.budget", "total_mb",
itoa_log((int)(cbm_mem_budget() / ((size_t)CBM_SZ_1K * CBM_SZ_1K))),
"per_worker_mb",
itoa_log((int)(worker_budget / ((size_t)CBM_SZ_1K * CBM_SZ_1K))));
}
/* Sub-phase: Ensure extraction library is initialized */
CBM_PROF_START(t_init);
cbm_init();
/* Slab allocator for tree-sitter (thread-safe via TLS). Destroy any
* parser this thread still holds BEFORE switching the global ts
* allocator: a parser created in the mimalloc epoch (sequential run,
* watcher tick) must be freed by the allocator that created it, or its
* teardown after the switch routes mi pointers into plain free()
* (#773). */
cbm_destroy_thread_parser();
cbm_slab_install();
CBM_PROF_END("parallel_extract", "1_init_libs", t_init);
/* Sub-phase: Sort files by descending size for tail-latency reduction */
CBM_PROF_START(t_sort);
file_sort_entry_t *sorted = malloc((size_t)file_count * sizeof(file_sort_entry_t));
if (!sorted) {
return CBM_NOT_FOUND;
}
for (int i = 0; i < file_count; i++) {
sorted[i].idx = i;
sorted[i].size = files[i].size;
}
qsort(sorted, file_count, sizeof(file_sort_entry_t), compare_by_size_desc);
CBM_PROF_END_N("parallel_extract", "2_sort_files", t_sort, file_count);
/* Allocate per-worker state (cache-line aligned via posix_memalign) */
extract_worker_state_t *workers = NULL;
if (cbm_aligned_alloc((void **)&workers, CBM_CACHE_LINE,
(size_t)worker_count * sizeof(extract_worker_state_t)) != 0) {
free(sorted);
return CBM_NOT_FOUND;
}
memset(workers, 0, (size_t)worker_count * sizeof(extract_worker_state_t));
/* Per-worker manifest entry arrays (separate from cache-line-aligned worker state) */
cbm_pkg_entries_t *pkg_entries = calloc((size_t)worker_count, sizeof(cbm_pkg_entries_t));
if (!pkg_entries) {
cbm_aligned_free(workers);
free(sorted);
return CBM_NOT_FOUND;
}
/* Per-worker skip lists (separate allocation; merged into the pipeline in the
* sequential merge loop below). */
pp_err_list_t *err_lists = calloc((size_t)worker_count, sizeof(pp_err_list_t));
/* ObjectScript macro table (NULL when no .inc include files present). */
CBMMacroTable *pp_macro_table =
cbm_build_macro_table_from_files(files, file_count, ctx->repo_path);
extract_ctx_t ec = {
.files = files,
.sorted = sorted,
.file_count = file_count,
.project_name = ctx->project_name,
.repo_path = ctx->repo_path,
.workers = workers,
.max_workers = worker_count,
.result_cache = result_cache,
.shared_ids = shared_ids,
.cancelled = ctx->cancelled,
.pctx = ctx,
.slot_state = NULL,
.pkg_entries = pkg_entries,
.err_lists = err_lists,
.retain_sources = resolved_opts.retain_sources,
.retain_total_budget_bytes = resolved_opts.retain_total_budget_bytes,
.retain_per_file_max_bytes = resolved_opts.retain_per_file_max_bytes,
.macro_table = pp_macro_table,
.return_type_table = ctx->return_type_table,
};
atomic_init(&ec.next_worker_id, 0);
atomic_init(&ec.next_file_idx, 0);
atomic_init(&ec.retained_bytes, 0);
atomic_init(&ec.retain_cap_warned, 0);
atomic_init(&ec.oversized_warned, 0);
atomic_init(&ec.bp_futile, 0);
atomic_init(&ec.over_budget_abort, 0);
atomic_init(&ec.spill_sweeps_running, 0);
atomic_init(&ec.spill_unavailable, 0);
cbm_mutex_init(&ec.spill_mu);
ec.slot_state = cbm_calloc(CBM_MEM_CLASS_OTHER, (size_t)file_count * sizeof(_Atomic uint8_t));
ec.spill_env = pp_spill_env_read();
if (pp_spill_forced_by_env(&ec)) {
pp_spill_enter(&ec, "env");
}
/* Sub-phase: Dispatch workers (parse + extract per file, PARALLEL) */
CBM_PROF_START(t_dispatch);
cbm_parallel_for_opts_t parallel_opts = {.max_workers = worker_count, .force_pthreads = false};
cbm_scale_begin(&ec.scale, "parallel_extract", (long)file_count);
cbm_parallel_for(worker_count, extract_worker, &ec, parallel_opts);
pp_spill_if_projected_over(&ec);
if (pp_spill_active(&ec) &&
!atomic_load_explicit(&ec.over_budget_abort, memory_order_relaxed)) {
/* Spill mode was entered, so results belong on disk: park every
* result still cached before the phases that cannot park (registry
* build, resolve, the semantic pass) inherit them. The sweeps above
* run only on an over-budget observation; a run that latched early
* and then stayed under budget through extraction (kernel, 15 GB,
* 2026-09-14: 14,949 MB at this point, 44,797 results = 8 GB still
* cached) reached resolve with no headroom and aborted there.
* The projection above enters spill mode here too when the phases
* after extraction would carry the charge over the latch (#2184). */
int parked = pp_spill_sweep(&ec, 0);
cbm_log_info("mem.spill.final_sweep", "parked", itoa_log(parked), "charged_mb",
itoa_log((int)(cbm_mem_charged() / ((size_t)1024 * 1024))));
}
if (ctx->spill) {
int64_t parked = 0;
int64_t bytes = 0;
cbm_result_spill_stats(ctx->spill, &parked, &bytes, NULL);
cbm_log_info("mem.spill.extract_done", "parked", itoa_log((int)parked), "mb",
itoa_log((int)(bytes / (1024 * 1024))));
}
cbm_mutex_destroy(&ec.spill_mu);
cbm_free(CBM_MEM_CLASS_OTHER, ec.slot_state);
ec.slot_state = NULL;
cbm_scale_end(&ec.scale);
CBM_PROF_END_N("parallel_extract", "3_dispatch_workers_parallel", t_dispatch, file_count);
/* Sub-phase: Merge all local gbufs into main gbuf (SEQUENTIAL, gbuf not thread-safe) */
CBM_PROF_START(t_merge);
int total_nodes = 0;
int total_errors = 0;
for (int i = 0; i < worker_count; i++) {
if (workers[i].local_gbuf) {
cbm_gbuf_merge(ctx->gbuf, workers[i].local_gbuf);
total_nodes += workers[i].nodes_created;
total_errors += workers[i].errors;
cbm_gbuf_free(workers[i].local_gbuf);
}
}
CBM_PROF_END_N("parallel_extract", "4_merge_gbufs_seq", t_merge, total_nodes);
/* Merge per-worker skip lists into the pipeline (SEQUENTIAL — no lock).
* Runs unconditionally (not gated on local_gbuf) so a worker whose files all
* failed still surfaces its skips. */
if (err_lists) {
for (int i = 0; i < worker_count; i++) {
for (int j = 0; j < err_lists[i].count; j++) {
cbm_pipeline_add_file_error(ctx->pipeline, err_lists[i].items[j].path,
err_lists[i].items[j].reason,
err_lists[i].items[j].phase);
free(err_lists[i].items[j].path);
free(err_lists[i].items[j].reason);
free(err_lists[i].items[j].phase);
}
free(err_lists[i].items);
}
free(err_lists);
}
merge_pkg_entries(ctx, pkg_entries, worker_count);
cbm_aligned_free(workers);
free(sorted);
cbm_macro_table_free(pp_macro_table); /* ObjectScript macro table (NULL-safe) */
/* The over-budget verdict outranks the cancel sentinel: a caller must be
* able to name the cause, and the orchestrator discards the staging DB on
* every non-zero code alike (the live generation is never touched). */
if (atomic_load(&ctx->test_declarations_failed)) {
cbm_log_error("pipeline.err", "phase", "test_declarations_extract");
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
if (atomic_load(&ec.over_budget_abort)) {
return CBM_PIPELINE_ABORT_OVER_BUDGET;
}
if (atomic_load(ctx->cancelled)) {
return CBM_NOT_FOUND;
}
log_extract_mem_stats(worker_count);
cbm_log_info("parallel.extract.done", "nodes", itoa_log(total_nodes), "errors",
itoa_log(total_errors));
return 0;
}
int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count,
CBMFileResult **result_cache, _Atomic int64_t *shared_ids,
int worker_count) {
return cbm_parallel_extract_ex(ctx, files, file_count, result_cache, shared_ids, worker_count,
NULL);
}
/* ── Phase 3B: Serial Registry Build ─────────────────────────────── */
/* Register one definition and create DEFINES + DEFINES_METHOD edges. Returns edge count. */
/* `file_node_id` is the defining file's node id (0 when it has none), looked up
* once per file by the caller: computing the file QN and finding its node for
* every definition was 700 k allocations and lookups on the Go corpus. */
static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
int64_t file_node_id, CBMLanguage lang, size_t module_prefix_len,
int *reg_entries) {
int edges = 0;
if (!def->name || !def->qualified_name || !def->label) {
return 0;
}
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
(*reg_entries)++;
}
char marked[CBM_SZ_4K];
const char *qn = strcmp(def->label, "Module") == 0
? cbm_stem_marks_module_qn(ctx->stem_marks, def->qualified_name,
def->file_path, lang, marked, sizeof(marked))
: def->qualified_name;
const cbm_gbuf_node_t *def_node = cbm_gbuf_find_by_qn(ctx->gbuf, qn);
if (file_node_id > 0 && def_node) {
cbm_gbuf_insert_edge(ctx->gbuf, file_node_id, def_node->id, "DEFINES", "{}");
edges++;
}
if (def->parent_class && strcmp(def->label, "Method") == 0) {
const cbm_gbuf_node_t *parent = cbm_gbuf_find_by_qn(ctx->gbuf, def->parent_class);
if (parent && def_node) {
cbm_gbuf_insert_edge(ctx->gbuf, parent->id, def_node->id, "DEFINES_METHOD", "{}");
}
}
if (def_node) {
cbm_pipeline_diagram_parent_edge(ctx, def, def_node->id);
}
return edges;
}
/* Add a file's own doc (Go package comment, Rust inner docs) to its File
* node as "docstring". Twin of pass_definitions.c -- keep both in sync. */
static void pp_add_file_doc(const cbm_gbuf_node_t *file_node, const char *doc) {
if (!file_node || !doc || !doc[0]) {
return;
}
const char *old = file_node->properties_json ? file_node->properties_json : "{}";
size_t olen = strlen(old);
if (olen < PAIR_LEN || old[olen - SKIP_ONE] != '}') {
return; /* not a JSON object -- leave it untouched */
}
size_t cap = olen + strlen("docstring") + pp_json_escaped_len(doc) + PP_JSON_FIELD_OVERHEAD +
PP_ESC_SPACE + SKIP_ONE;
char *neu = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, cap);
if (!neu) {
return;
}
size_t pos = olen - SKIP_ONE; /* without the closing brace */
memcpy(neu, old, pos);
neu[pos] = '\0';
append_json_string(neu, cap, &pos, "docstring", doc);
if (olen == PAIR_LEN && pos > PAIR_LEN) { /* "{}": drop the leading comma */
memmove(neu + SKIP_ONE, neu + PAIR_LEN, pos - SKIP_ONE);
pos--;
}
neu[pos++] = '}';
neu[pos] = '\0';
(void)cbm_gbuf_node_set_properties_json((cbm_gbuf_node_t *)file_node, neu);
cbm_free(CBM_MEM_CLASS_GBUF_STRING, neu);
}
/* Create IMPORTS edges for one file's imports (parallel path). */
static int create_imports_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel, CBMHashTable *namespace_map) {
int count = 0;
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
if (!source_node) {
free(file_qn);
return 0;
}
if (cbm_language_for_filename(rel) == CBM_LANG_PHP) {
count = cbm_pipeline_php_create_import_edges(ctx, result, rel, file_qn, source_node,
namespace_map);
} else {
for (int j = 0; j < result->imports.count; j++) {
CBMImport *imp = &result->imports.items[j];
if (!imp->module_path) {
continue;
}
const cbm_gbuf_node_t *target =
cbm_pipeline_resolve_import_node(ctx, rel, file_qn, imp, namespace_map);
if (target && target->id != source_node->id) {
char esc_ln[CBM_SZ_128];
cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : "");
char imp_props[CBM_SZ_256];
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln);
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
count++;
}
}
}
free(file_qn);
return count;
}
/* Find channel source node (enclosing function or file). */
static const cbm_gbuf_node_t *find_channel_src(cbm_pipeline_ctx_t *ctx, const CBMChannel *ch,
const char *rel) {
const cbm_gbuf_node_t *node = NULL;
if (ch->enclosing_func_qn && ch->enclosing_func_qn[0]) {
node = cbm_gbuf_find_by_qn(ctx->gbuf, ch->enclosing_func_qn);
}
if (!node) {
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
free(file_qn);
}
return node;
}
/* Create Channel nodes + EMITS/LISTENS_ON edges for one file. */
static void create_channel_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel) {
for (int j = 0; j < result->channels.count; j++) {
CBMChannel *ch = &result->channels.items[j];
if (!ch->channel_name || !ch->channel_name[0]) {
continue;
}
char channel_qn[CBM_SZ_512];
snprintf(channel_qn, sizeof(channel_qn), "__channel__%s__%s",
ch->transport ? ch->transport : "unknown", ch->channel_name);
char esc_cn[CBM_SZ_256];
cbm_json_escape(esc_cn, sizeof(esc_cn), ch->channel_name);
char channel_props[CBM_SZ_512];
snprintf(channel_props, sizeof(channel_props), "{\"transport\":\"%s\",\"name\":\"%s\"}",
ch->transport ? ch->transport : "unknown", esc_cn);
int64_t channel_id = cbm_gbuf_upsert_node(ctx->gbuf, "Channel", ch->channel_name,
channel_qn, "", 0, 0, channel_props);
const cbm_gbuf_node_t *src_node = find_channel_src(ctx, ch, rel);
if (src_node && channel_id > 0) {
const char *edge_type = ch->direction == CBM_CHANNEL_EMIT ? "EMITS" : "LISTENS_ON";
char edge_props[CBM_SZ_128];
snprintf(edge_props, sizeof(edge_props), "{\"transport\":\"%s\"}",
ch->transport ? ch->transport : "unknown");
cbm_gbuf_insert_edge(ctx->gbuf, src_node->id, channel_id, edge_type, edge_props);
}
}
}
int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count, CBMFileResult **result_cache) {
cbm_log_info("parallel.registry.start", "files", itoa_log(file_count));
int reg_entries = 0;
int defines_edges = 0;
int imports_edges = 0;
/* Namespace/package → File-QN map for namespace imports (C# `using`,
* Java/Kotlin `import`, PHP `use`). Built from the full result cache so
* every declaring file is visible regardless of loop order. */
const char **rels = (const char **)calloc((size_t)file_count, sizeof(char *));
if (rels) {
for (int i = 0; i < file_count; i++) {
rels[i] = files[i].rel_path;
}
}
/* Built from every file, including the ones already parked on disk: their
* results are NULL in result_cache, and a file absent from this map does
* not fail to resolve, it resolves through the looser fallback. Spilling
* therefore used to CHANGE the graph rather than merely delay it -- php
* measured 57,182 edges in memory against 59,379 while spilling, the same
* binary and corpus (2026-09-18), differing in both directions. */
const char **namespaces = cbm_calloc(CBM_MEM_CLASS_OTHER, (size_t)file_count * sizeof(char *));
CBMHashTable *namespace_map = NULL;
if (namespaces) {
for (int i = 0; i < file_count; i++) {
namespaces[i] = result_cache[i] ? result_cache[i]->namespace_name
: cbm_result_spill_namespace(ctx->spill, i);
}
namespace_map =
cbm_pipeline_namespace_map_build_names(ctx->project_name, namespaces, rels, file_count);
cbm_free(CBM_MEM_CLASS_OTHER, namespaces);
}
free(rels);
for (int i = 0; i < file_count; i++) {
if (cbm_pipeline_check_cancel(ctx)) {
cbm_pipeline_namespace_map_free(namespace_map);
return CBM_NOT_FOUND;
}
CBMFileResult *result = result_cache[i];
bool loaded = false;
if (!result && ctx->spill && cbm_result_spill_has(ctx->spill, i)) {
result = cbm_result_spill_load(ctx->spill, i);
loaded = result != NULL;
if (!cbm_pipeline_test_extraction_ok(ctx, files[i].language, result)) {
cbm_pipeline_result_release(result, loaded);
cbm_pipeline_namespace_map_free(namespace_map);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
}
if (!result) {
continue;
}
if (!cbm_pipeline_test_result_ok(ctx, result)) {
cbm_pipeline_result_release(result, loaded);
cbm_pipeline_namespace_map_free(namespace_map);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
const char *rel = files[i].rel_path;
/* Register callable symbols + DEFINES/DEFINES_METHOD edges */
if (result->defs.count > 0) {
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
int64_t file_node_id = file_node ? file_node->id : 0;
free(file_qn);
pp_add_file_doc(file_node, result->module_doc);
/* Bounds the function-local detection to this file (pass_definitions.c). */
size_t py_prefix_len = files[i].language == CBM_LANG_PYTHON
? cbm_pipeline_fqn_symbol_prefix_len(ctx->project_name, rel)
: 0;
for (int d = 0; d < result->defs.count; d++) {
defines_edges +=
register_and_link_def(ctx, &result->defs.items[d], file_node_id,
files[i].language, py_prefix_len, &reg_entries);
}
}
imports_edges += create_imports_edges(ctx, result, rel, namespace_map);
create_channel_edges(ctx, result, rel);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
if (loaded) {
cbm_free_result(result);
}
}
/* Publish instead of free: the call pass needs the same declared-package
* set to tell an in-tree package path from a third-party one (#1355).
* The pipeline owns it from here and frees it in its cleanup. */
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(namespace_map);
cbm_log_info("parallel.registry.done", "entries", itoa_log(reg_entries), "defines",
itoa_log(defines_edges), "imports", itoa_log(imports_edges));
return 0;
}
/* ── Phase 4: Parallel Resolution ────────────────────────────────── */
typedef struct __attribute__((aligned(CBM_CACHE_LINE))) {
cbm_gbuf_t *local_edge_buf;
int calls_resolved;
int usages_resolved;
int semantic_resolved;
int errors;
/* Subset of calls_resolved that were attributed via the LSP-override
* path (cbm_pipeline_find_lsp_resolution hit) rather than the
* registry's textual matcher. Surfaced in the parallel.resolve.done
* log line so divergence between pipelines becomes observable. */
int lsp_overrides;
CBMFileResult *loaded; /* spill-loaded result this worker is using */
char _pad[CBM_CACHE_LINE - 2 * sizeof(void *) - ((PP_RING + 1) * sizeof(int))];
} resolve_worker_state_t;
typedef struct {
const cbm_file_info_t *files;
int file_count;
const char *project_name;
const char *repo_path;
resolve_worker_state_t *workers;
int max_workers;
CBMFileResult **result_cache;
const cbm_gbuf_t *main_gbuf; /* READ-ONLY during Phase 4 */
const cbm_registry_t *registry; /* READ-ONLY during Phase 4 */
_Atomic int64_t *shared_ids;
_Atomic int *cancelled;
_Atomic int next_file_idx;
/* Cross-file LSP inputs — pre-built once by the caller in pipeline.c
* and shared read-only by usage across workers (typed non-const to
* match the existing cbm_run_X_lsp_cross callee signatures the
* worker forwards them to). NULL/0 → cross-LSP no-ops. */
CBMLSPDef *all_defs;
int def_count;
char *const *def_modules; /* per-file module QN; def_modules[i] for files[i] */
/* Optional inverted index for per-file def filtering (gopls pattern).
* When non-NULL, the fused worker calls cbm_pxc_filter_defs_for_file
* to shrink the def array passed to the LSP from O(all_defs) to
* O(relevant_defs). NULL → each file sees the full all_defs[]. */
struct CBMModuleDefIndex *module_def_index;
/* Tier 2 full: pre-built per-language registries (project-wide,
* finalized, READ-ONLY). When non-NULL for a lang, the worker uses
* cbm_run_X_lsp_cross_with_registry — skip per-file build entirely.
* Stored as CBMCrossLspRegistries* (typedef from pass_lsp_cross.h). */
CBMCrossLspRegistries *cross_registries;
/* Parsed once on the coordinator thread and borrowed read-only by resolve
* workers. The pointer is installed into each worker's TLS slot so Rust's
* manifest-aware cross-file resolver sees the same Cargo context. */
const CBMCargoManifest *rust_manifest;
/* F4: LAZILY-built shared Rust registry (built ONCE, on the first NULL-filter
* rust file — the ~all_defs amplifier files). Not eager: repos whose rust files
* all filter to subsets never pay the O(all_defs) build + multi-GB RSS. Built
* under rust_shared_mu into rust_shared_arena, published via rust_shared_reg;
* torn down after the worker dispatch. */
_Atomic(CBMTypeRegistry *) rust_shared_reg;
cbm_mutex_t rust_shared_mu;
CBMArena rust_shared_arena;
bool rust_shared_arena_live;
/* Counters for parallel.resolve.lsp_cross_done summary. */
_Atomic int lsp_cross_processed;
_Atomic int lsp_cross_skipped_no_source;
/* Each file is owned by one worker. The coordinator reports failures
* after join, without mutating the pipeline's error list concurrently. */
const char **lsp_failures;
/* Per-sub-phase timing (ns aggregated across workers) — surfaces
* exactly where parallel_resolve's wall time is spent so we stop
* guessing about hot paths. Logged once at the end of
* cbm_parallel_resolve. */
_Atomic uint64_t time_ns_import_map;
_Atomic uint64_t time_ns_cross_lsp;
_Atomic uint64_t time_ns_calls;
_Atomic uint64_t time_ns_usages;
_Atomic uint64_t time_ns_throws;
_Atomic uint64_t time_ns_rw;
_Atomic uint64_t time_ns_semantic;
/* Whole-iteration timer — captures everything from atomic file_idx
* pickup through cleanup. If this >> sum of sub-phases, the
* unmeasured cost is either in skip-eligibility checks, gbuf
* setup, or — most likely — workers waiting on the
* cbm_parallel_for synchronization barrier at the end. */
_Atomic uint64_t time_ns_total_loop;
_Atomic int total_files_visited;
/* Sub-breakdowns inside resolve_file_calls — finds the 553µs-per-
* iteration hot path that the high-level resolve_calls counter
* doesn't pinpoint. */
_Atomic uint64_t time_ns_rc_lsp_lookup; /* lsp_idx + fallback scan */
_Atomic uint64_t time_ns_rc_resolve; /* lsp_target_node OR registry_resolve */
_Atomic uint64_t time_ns_rc_hint; /* try_field_type_hint */
_Atomic uint64_t time_ns_rc_target; /* gbuf_find_by_qn for target */
_Atomic uint64_t time_ns_rc_emit; /* emit_service_edge */
_Atomic uint64_t time_ns_rc_source; /* find_source_node */
/* Superlinearity probe — see profile.h. This is the pass that made it
* necessary (#1669: 87% of a Java index), so it is the one that must never
* again grow superlinear without saying so. */
cbm_scale_probe_t scale;
cbm_pipeline_ctx_t *pctx; /* spill store access */
} resolve_ctx_t;
/* Minimum buffer space needed per arg JSON object */
#define CBM_ARG_JSON_GUARD CBM_SZ_32
/* Append arg data as JSON to edge properties: ,"args":[{"i":0,"e":"x","v":"val"},...]
* Returns new position in buffer. */
/* Sanitize expression string for JSON (in-place). */
static void sanitize_expr(char *expr_buf, const char *expr) {
if (expr) {
snprintf(expr_buf, 128, "%.*s", 120, expr);
/* The 120-byte cut can land inside a multibyte character; a torn
* sequence persisted as invalid UTF-8 in edge properties (2026-09-16
* probe: rust, java, typescript stores). */
cbm_utf8_trim_partial(expr_buf);
for (char *p = expr_buf; *p; p++) {
if (*p == '"') {
*p = '\'';
}
if (*p == '\n' || *p == '\r') {
*p = ' ';
}
}
} else {
expr_buf[0] = '\0';
}
}
/* Format one call arg as JSON. Returns snprintf result. */
static int format_call_arg(char *buf, size_t bufsize, const CBMCallArg *a, const char *expr) {
char esc_k[CBM_SZ_128];
char esc_e[CBM_SZ_128];
char esc_v[CBM_SZ_128];
cbm_json_escape(esc_e, sizeof(esc_e), expr);
if (a->keyword && a->value) {
cbm_json_escape(esc_k, sizeof(esc_k), a->keyword);
cbm_json_escape(esc_v, sizeof(esc_v), a->value);
return snprintf(buf, bufsize, "{\"i\":%d,\"k\":\"%s\",\"e\":\"%s\",\"v\":\"%s\"}", a->index,
esc_k, esc_e, esc_v);
}
if (a->keyword) {
cbm_json_escape(esc_k, sizeof(esc_k), a->keyword);
return snprintf(buf, bufsize, "{\"i\":%d,\"k\":\"%s\",\"e\":\"%s\"}", a->index, esc_k,
esc_e);
}
if (a->value) {
cbm_json_escape(esc_v, sizeof(esc_v), a->value);
return snprintf(buf, bufsize, "{\"i\":%d,\"e\":\"%s\",\"v\":\"%s\"}", a->index, esc_e,
esc_v);
}
return snprintf(buf, bufsize, "{\"i\":%d,\"e\":\"%s\"}", a->index, esc_e);
}
static size_t append_args_json(char *buf, size_t bufsize, size_t pos, const CBMCall *call) {
if (call->arg_count == 0 || pos >= bufsize - PP_ARGS_MARGIN) {
return pos;
}
int n = snprintf(buf + pos, bufsize - pos, ",\"args\":[");
if (n <= 0) {
return pos;
}
pos += (size_t)n;
for (int i = 0; i < call->arg_count && pos < bufsize - CBM_ARG_JSON_GUARD; i++) {
const CBMCallArg *a = &call->args[i];
size_t mark = pos; /* rollback point (before the separator) */
if (i > 0 && pos < bufsize - SKIP_ONE) {
buf[pos++] = ',';
}
char expr_buf[CBM_SZ_128];
sanitize_expr(expr_buf, a->expr);
n = format_call_arg(buf + pos, bufsize - pos, a, expr_buf);
/* snprintf returns the UNtruncated length: if the arg did not fully
* fit, advancing pos by n would push it past buf and the buf[pos]
* writes below would overflow. Drop the arg whole (atomic field —
* keeps the array valid) and stop appending. */
if (n <= 0 || (size_t)n >= bufsize - pos) {
pos = mark;
break;
}
pos += (size_t)n;
}
if (pos < bufsize - SKIP_ONE) {
buf[pos++] = ']';
}
buf[pos] = '\0';
return pos;
}
/* Scan call args for a URL-like route path and handler reference. */
static bool is_path_keyword(const char *keyword) {
static const char *path_keywords[] = {"prefix", "path", "route", "pattern",
"url", "endpoint", "rule", "mount_path",
"route_path", "url_path", NULL};
for (const char **kw = path_keywords; *kw; kw++) {
if (strcmp(keyword, *kw) == 0) {
return true;
}
}
return false;
}
/* A route path is one line that opens with a slash. A block or line comment
* opens with a slash too, and an argument list that starts with one used to
* hand the comment text to the Route pass (three Java block comments became
* Route nodes on elasticsearch, 2026-09-16). */
static bool is_route_path_shaped(const char *val) {
return val && val[0] == '/' && !cbm_service_pattern_is_comment_text(val);
}
static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler) {
*out_handler = NULL;
/* 1. First string arg starting with / */
if (is_route_path_shaped(call->first_string_arg)) {
*out_handler = call->second_arg_name;
return call->first_string_arg;
}
/* 2. Keyword args (prefix=, path=, route=, etc.) */
const char *found = NULL;
for (int ai = 0; ai < call->arg_count && !found; ai++) {
const CBMCallArg *ca = &call->args[ai];
const char *val = ca->value ? ca->value : ca->expr;
if (!is_route_path_shaped(val)) {
continue;
}
if ((ca->keyword && is_path_keyword(ca->keyword)) || (!ca->keyword && ca->index == 0)) {
found = val;
}
}
if (!found) {
return NULL;
}
/* 3. Handler: first identifier arg that's not a path/keyword */
for (int ai = 0; ai < call->arg_count; ai++) {
const CBMCallArg *ca = &call->args[ai];
if (!ca->expr || ca->expr[0] == '/' || ca->expr[0] == '"' || ca->expr[0] == '\'') {
continue;
}
if (ca->keyword && (strcmp(ca->keyword, "prefix") == 0 ||
strcmp(ca->keyword, "name") == 0 || strcmp(ca->keyword, "tags") == 0)) {
continue;
}
*out_handler = ca->expr;
break;
}
return found;
}
/* Append the call's args (and, for CALLS, its line) to props and close the
* brace. Shared with the sequential pass, whose service edges must carry the
* same bytes (pass_calls.c emit_http_async_edge). */
void cbm_pipeline_close_call_props(char *props, int n, const CBMCall *call, bool with_line) {
if (n > 0 && (size_t)n < CBM_SZ_2K - PP_ESC_SPACE) {
size_t pos = append_args_json(props, CBM_SZ_2K, (size_t)n, call);
if (with_line && call->start_line > 0 && pos < CBM_SZ_2K - PP_LINE_MARGIN) {
int ln = snprintf(props + pos, CBM_SZ_2K - pos, ",\"line\":%d", call->start_line);
if (ln > 0) {
pos += (size_t)ln;
}
}
if (pos < CBM_SZ_2K - SKIP_ONE) {
props[pos] = '}';
props[pos + SKIP_ONE] = '\0';
}
}
}
/* Build props JSON, append args, close brace, emit edge. */
static bool finalize_and_emit(cbm_gbuf_t *gbuf, int64_t src_id, int64_t tgt_id,
const char *edge_type, char *props, int n, const CBMCall *call) {
cbm_pipeline_close_call_props(props, n, call, strcmp(edge_type, "CALLS") == 0);
return cbm_gbuf_insert_edge(gbuf, src_id, tgt_id, edge_type, props) > 0 &&
strcmp(edge_type, "CALLS") == 0;
}
/* Build Route node QN and properties for HTTP/async service edges. */
static int64_t build_service_route(cbm_gbuf_t *gbuf, const char *arg, const char *method,
const char *broker, cbm_svc_kind_t svc) {
char route_qn[CBM_ROUTE_QN_SIZE];
const char *prefix;
char cpath[CBM_SZ_256];
const char *qpath = arg;
if (svc == CBM_SVC_HTTP) {
prefix = method ? method : "ANY";
qpath = cbm_route_canon_path(arg, cpath, sizeof(cpath));
} else {
prefix = broker ? broker : "async";
}
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix, qpath);
char route_props[CBM_SZ_256];
if (method) {
snprintf(route_props, sizeof(route_props), "{\"method\":\"%s\"}", method);
} else if (broker) {
snprintf(route_props, sizeof(route_props), "{\"broker\":\"%s\"}", broker);
} else {
snprintf(route_props, sizeof(route_props), "{}");
}
return cbm_gbuf_upsert_node(gbuf, "Route", arg, route_qn, "", 0, 0, route_props);
}
/* Emit HTTP_CALLS or ASYNC_CALLS edge via Route node. */
static void emit_http_async_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const CBMCall *call, const cbm_resolution_t *res,
cbm_svc_kind_t svc, const char *arg) {
const char *edge_type = (svc == CBM_SVC_HTTP) ? "HTTP_CALLS" : "ASYNC_CALLS";
const char *method =
(svc == CBM_SVC_HTTP) ? cbm_service_pattern_http_method(call->callee_name) : NULL;
const char *broker =
(svc == CBM_SVC_ASYNC) ? cbm_service_pattern_broker(res->qualified_name) : NULL;
int64_t route_id = build_service_route(gbuf, arg, method, broker, svc);
char esc_c[CBM_SZ_256];
char esc_a[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_a, sizeof(esc_a), arg);
char props[CBM_SZ_2K];
int n = snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"url_path\":\"%s\"", esc_c, esc_a);
if (method) {
n += snprintf(props + n, sizeof(props) - (size_t)n, ",\"method\":\"%s\"", method);
}
if (broker) {
n += snprintf(props + n, sizeof(props) - (size_t)n, ",\"broker\":\"%s\"", broker);
}
finalize_and_emit(gbuf, source->id, route_id, edge_type, props, n, call);
}
/* Emit CONFIGURES edge. */
static void emit_config_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const cbm_gbuf_node_t *target, const CBMCall *call,
const cbm_resolution_t *res, const char *arg) {
/* emit_service_edge may be reached with target==NULL on the HTTP/ASYNC
* external-client bypass (#523); a CONFIGURES edge needs a real target, so
* never deref a NULL target here. */
if (!target) {
return;
}
char esc_c[CBM_SZ_256];
char esc_k[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_k, sizeof(esc_k), arg ? arg : "");
char props[CBM_SZ_2K];
int n = snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"key\":\"%s\",\"confidence\":%.2f",
esc_c, esc_k, res->confidence);
finalize_and_emit(gbuf, source->id, target->id, "CONFIGURES", props, n, call);
}
/* Emit normal CALLS edge. */
static bool emit_normal_calls_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const cbm_gbuf_node_t *target, const CBMCall *call,
const cbm_resolution_t *res) {
/* A CALLS edge needs a real target; the HTTP/ASYNC external-client bypass
* (#523) can reach emit_service_edge with target==NULL, so guard the deref. */
if (!target) {
return false;
}
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_2K];
int n = snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"confidence\":%.2f,\"strategy\":\"%s\",\"candidates\":%d",
esc_c, res->confidence, res->strategy ? res->strategy : "unknown",
res->candidate_count);
return finalize_and_emit(gbuf, source->id, target->id, "CALLS", props, n, call);
}
/* Classify a resolved call by library identity and emit the appropriate edge. */
/* Create Route node + CALLS + HANDLES edges for a route registration call.
* route_mount is the framework mount of the registering file ("/api" for a
* Laravel 11+ `withRouting(api: ...)` file, #1146) or "". */
static bool emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const CBMCall *call, const char *route_path,
const char *handler_ref, const char *module_qn,
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
const char **ik, const char **iv, int ic,
const char *route_mount, CBMLanguage lang) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
char mounted[CBM_SZ_256];
route_path = cbm_laravel_mount_route(route_mount, route_path, mounted, sizeof(mounted));
char rqn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(rqn, sizeof(rqn), "__route__%s__%s", method ? method : "ANY",
cbm_route_canon_path(route_path, cpath, sizeof(cpath)));
char rp[CBM_SZ_256];
snprintf(rp, sizeof(rp), "{\"method\":\"%s\"}", method ? method : "ANY");
int64_t rid = cbm_gbuf_upsert_node(gbuf, "Route", route_path, rqn, "", 0, 0, rp);
char esc_cn[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
char esc_rp[CBM_SZ_512];
cbm_json_escape(esc_cn, sizeof(esc_cn), call->callee_name);
cbm_json_escape(esc_rp, sizeof(esc_rp), route_path);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"route_registration\"}", esc_cn,
esc_rp);
bool emitted = cbm_gbuf_insert_edge(gbuf, source->id, rid, "CALLS", props) > 0;
if (handler_ref && handler_ref[0] != '\0') {
/* Python: a handler an app factory registers is usually its local def
* (`app.add_url_rule("/", "index", index)` inside create_app); only the
* lexical lookup sees a function's locals. MUST match pass_calls.c. */
cbm_resolution_t hres = {0};
if (lang == CBM_LANG_PYTHON) {
hres = cbm_registry_resolve_lexical(registry, handler_ref, call->enclosing_func_qn,
module_qn);
}
if (!hres.qualified_name) {
hres = cbm_registry_resolve_handler(registry, handler_ref, module_qn, ik, iv, ic,
main_gbuf, lang);
}
if (hres.qualified_name && hres.qualified_name[0] != '\0') {
const cbm_gbuf_node_t *h = cbm_gbuf_find_by_qn(main_gbuf, hres.qualified_name);
if (h) {
char hp[CBM_SZ_1K]; /* must exceed escaped value + wrapper or snprintf cuts the
closing brace */
char esc_h2[CBM_SZ_512];
cbm_json_escape(esc_h2, sizeof(esc_h2), hres.qualified_name);
snprintf(hp, sizeof(hp), "{\"handler\":\"%s\"}", esc_h2);
cbm_gbuf_insert_edge(gbuf, h->id, rid, "HANDLES", hp);
}
}
}
return emitted;
}
/* Reject regex metacharacters, spaces, double-slashes in URL candidates. */
static bool is_junk_url(const char *s) {
for (int i = 0; s[i]; i++) {
char ch = s[i];
if (ch == '\\' || ch == '^' || ch == '$' || ch == '*' || ch == '+' || ch == '(' ||
ch == ')' || ch == '[' || ch == ']' || ch == '|' || ch == ' ') {
return true;
}
if (ch == '/' && i > 0 && s[i - SKIP_ONE] == '/') {
return true;
}
}
return false;
}
/* Normalize a template literal URL and reject junk patterns.
* Returns true if norm contains a valid API path. */
static bool normalize_url_arg(const char *url, char *norm, int norm_sz) {
int ni = 0;
const char *p = url;
if (*p == '`' || *p == '"' || *p == '\'') {
p++;
}
if (*p != '/') {
return false;
}
while (*p && ni < norm_sz - PAIR_LEN) {
if (*p == '$' && *(p + SKIP_ONE) == '{') {
norm[ni++] = ':';
p += PAIR_LEN;
while (*p && *p != '}' && ni < norm_sz - PAIR_LEN) {
norm[ni++] = *p++;
}
if (*p == '}') {
p++;
}
} else if (*p == '`' || *p == '"' || *p == '\'' || *p == '?') {
break;
} else {
norm[ni++] = *p++;
}
}
norm[ni] = '\0';
enum { MIN_URL_LEN = 4 };
if (ni < MIN_URL_LEN || !strchr(norm + SKIP_ONE, '/')) {
return false;
}
return !is_junk_url(norm);
}
static bool arg_is_topic(const CBMCallArg *arg) {
static const char *keywords[] = {"topic", "topic_id", "topic_name", "queue", "queue_name",
"queue_id", "subject", "channel", NULL};
for (int i = 0; arg->keyword && keywords[i]; i++) {
if (strcmp(arg->keyword, keywords[i]) == 0) {
return true;
}
}
return false;
}
/* File-language scope of the client-navigation heuristic (#1250): the JS/TS
* family and the hosts that embed it. Kept explicit so changes to the
* weak-member guard cannot broaden it. */
static bool pp_client_navigation_lang(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS || lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
}
/* Detect API paths in call arguments and create HTTP_CALLS edges. */
static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
cbm_svc_kind_t svc, bool js_family) {
/* Only the generic argument heuristic is suppressed: explicit HTTP service
* classification above this call still emits its edge. */
if (cbm_service_pattern_is_client_navigation(js_family, call->callee_name)) {
return;
}
/* A resolved alias may be ASYNC even when its source spelling is unknown.
* Neither raw slash topics nor URL projections are HTTP evidence there. */
if (svc == CBM_SVC_ASYNC || cbm_service_pattern_match(call->callee_name) == CBM_SVC_ASYNC) {
return;
}
for (int ai = 0; ai < call->arg_count; ai++) {
const CBMCallArg *ca = &call->args[ai];
if (arg_is_topic(ca)) {
continue;
}
/* A slash-prefixed raw expression is not a URL string. In JS/TS this
* is notably a regex literal (`/<table/i`); genuine string literals
* and propagated constants are carried in `value`, while template
* literals keep their leading backtick in `expr`. */
if (!ca->value && !ca->url_value && ca->expr && ca->expr[0] == '/') {
continue;
}
const char *url = ca->url_value ? ca->url_value : (ca->value ? ca->value : ca->expr);
if (!url || (url[0] != '/' && url[0] != '`')) {
continue;
}
char norm[CBM_SZ_256];
if (!normalize_url_arg(url, norm, (int)sizeof(norm))) {
continue;
}
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
continue;
}
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
"{\"source\":\"arg_url\"}");
char esc_c[CBM_SZ_256];
char esc_n[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_n, sizeof(esc_n), norm);
char eprops[CBM_SZ_512];
snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "HTTP_CALLS", eprops);
break;
}
}
/* Extract gRPC service and method from a callee name.
* Handles patterns like: pb.NewFooServiceClient(conn).GetBar → Foo/GetBar
* Also: FooServiceGrpc.newBlockingStub(ch).getBar → FooService/getBar */
bool extract_grpc_service_method(const char *callee, char *service, size_t srv_sz, char *method,
size_t meth_sz) {
service[0] = '\0';
method[0] = '\0';
if (!callee) {
return false;
}
/* Find last dot to split service.Method */
const char *last_dot = strrchr(callee, '.');
if (!last_dot || !last_dot[SKIP_ONE]) {
return false;
}
snprintf(method, meth_sz, "%s", last_dot + SKIP_ONE);
/* Extract service name: everything before the last dot, stripped of prefixes/suffixes */
size_t prefix_len = (size_t)(last_dot - callee);
char raw[CBM_SZ_256];
if (prefix_len >= sizeof(raw)) {
prefix_len = sizeof(raw) - SKIP_ONE;
}
memcpy(raw, callee, prefix_len);
raw[prefix_len] = '\0';
/* Strip common prefixes: pb.New, New, pb. */
const char *s = raw;
if (strncmp(s, "pb.New", CBM_SZ_6) == 0) {
s += CBM_SZ_6;
} else if (strncmp(s, "pb.", CBM_SZ_3) == 0 || strncmp(s, "New", CBM_SZ_3) == 0) {
s += CBM_SZ_3;
}
/* Strip the generated-stub/client suffix, preserving the canonical
* proto-declared service name. The proto service is `<X>Service`; the
* generated client is `<X>ServiceClient` / `<X>ServiceGrpc`, so we strip
* only the trailing stub/client token (Client/Stub/Grpc/…), NOT "Service"
* itself — stripping "ServiceClient" yielded `<X>` and broke cross-repo
* matching against the `<X>Service` declared name (#294). Longest tokens
* first so e.g. BlockingStub wins over Stub.
*
* A match also serves as the gRPC stub-type signal: we ONLY emit a Route
* when a recognized suffix is actually present. Without this gate the
* fallback turned ordinary receiver vars (`_provider.GetGroup`,
* `_builder.AddSomeService`) into phantom `__grpc__provider/...` Routes
* that correspond to no .proto anywhere (#294). */
snprintf(service, srv_sz, "%s", s);
size_t slen = strlen(service);
static const char *const suffixes[] = {"BlockingStub", "FutureStub", "AsyncStub",
"AsyncClient", "Servicer", "Client",
"Stub", "Grpc", NULL};
bool stripped = false;
for (const char *const *sfx = suffixes; *sfx; sfx++) {
size_t flen = strlen(*sfx);
if (slen > flen && strcmp(service + slen - flen, *sfx) == 0) {
service[slen - flen] = '\0';
stripped = true;
break;
}
}
return stripped && service[0] && method[0];
}
/* Emit GRPC_CALLS edge via gRPC Route node. */
static void emit_grpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
char service[CBM_SZ_256];
char method[CBM_SZ_256];
/* Try callee_name first (e.g., "pb.NewCartServiceClient.GetCart") */
if (!extract_grpc_service_method(call->callee_name, service, sizeof(service), method,
sizeof(method))) {
/* Fallback: try the resolved QN for Go chained calls.
* Go pattern: pb.NewCartServiceClient(conn).GetCart(ctx, req)
* callee_name = "GetCart", QN = "...CartServiceClient.GetCart"
* The QN contains the full ServiceClient.Method pattern. */
if (!res->qualified_name ||
!extract_grpc_service_method(res->qualified_name, service, sizeof(service), method,
sizeof(method))) {
return;
}
}
char route_qn[CBM_SZ_512];
snprintf(route_qn, sizeof(route_qn), "__grpc__%s/%s", service, method);
char route_name[CBM_SZ_256];
snprintf(route_name, sizeof(route_name), "%s/%s", service, method);
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", route_name, route_qn, "", 0, 0,
"{\"source\":\"grpc\"}");
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"service\":\"%s\",\"method\":\"%s\",\"confidence\":%.2f}", esc_c,
service, method, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "GRPC_CALLS", props);
}
/* Emit GRAPHQL_CALLS edge. The Route is keyed by the operation NAME only
* (#598), never by the operation text:
* named operation -> __graphql__<Name> name "<Name>"
* anonymous query/mutation/ -> __graphql__<type>__anonymous name "(anonymous <type>)"
* subscription (incl. the `{ ... }` query shorthand)
* no GraphQL document -> __graphql__operation__anonymous name "(anonymous operation)"
* (URL, variable, missing string argument)
* Keys are bounded by the GraphQL Name grammar and deterministic. */
static void emit_graphql_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
const char *op_type = "operation";
char op_name[CBM_SZ_128];
bool named = cbm_service_pattern_graphql_operation(call->first_string_arg, &op_type, op_name,
sizeof(op_name));
char route_name[CBM_SZ_256];
char route_qn[CBM_SZ_256];
if (named) {
snprintf(route_name, sizeof(route_name), "%s", op_name);
snprintf(route_qn, sizeof(route_qn), "__graphql__%s", op_name);
} else {
snprintf(route_name, sizeof(route_name), "(anonymous %s)", op_type);
snprintf(route_qn, sizeof(route_qn), "__graphql__%s__anonymous", op_type);
}
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", route_name, route_qn, "", 0, 0,
"{\"source\":\"graphql\"}");
char esc_c[CBM_SZ_256];
char esc_op[CBM_SZ_512];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_op, sizeof(esc_op), route_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"operation\":\"%s\",\"operation_type\":\"%s\","
"\"confidence\":%.2f}",
esc_c, esc_op, op_type, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "GRAPHQL_CALLS", props);
}
/* Emit TRPC_CALLS edge. Extract procedure path from callee chain. */
static void emit_trpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
/* tRPC calls: trpc.user.getById.query() → extract "user.getById" */
const char *callee = call->callee_name;
if (!callee) {
return;
}
/* Strip trailing .query/.mutate/.subscribe */
char proc[CBM_SZ_256];
snprintf(proc, sizeof(proc), "%s", callee);
char *last_dot = strrchr(proc, '.');
if (last_dot && (strcmp(last_dot, ".query") == 0 || strcmp(last_dot, ".mutate") == 0 ||
strcmp(last_dot, ".subscribe") == 0 || strcmp(last_dot, ".useQuery") == 0 ||
strcmp(last_dot, ".useMutation") == 0)) {
*last_dot = '\0';
}
/* Strip leading trpc. */
const char *p = proc;
if (strncmp(p, "trpc.", CBM_SZ_5) == 0) {
p += CBM_SZ_5;
}
char route_qn[CBM_SZ_512];
snprintf(route_qn, sizeof(route_qn), "__trpc__%s", p);
int64_t route_id =
cbm_gbuf_upsert_node(gbuf, "Route", p, route_qn, "", 0, 0, "{\"source\":\"trpc\"}");
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"procedure\":\"%s\",\"confidence\":%.2f}",
esc_c, p, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "TRPC_CALLS", props);
}
/* When suppress_plain_calls is true (a TS/JS/TSX weak short-name member-call
* match, #592/#606), every service classification below still runs — only the
* plain CALLS fall-through (emit_normal_calls_edge) is skipped. detect_url_in_args
* and the HTTP/ASYNC/gRPC/GraphQL/tRPC/CONFIG/route branches are unaffected, so
* a verb-suffix HTTP client (api.patch('/x')), broker, or route registration
* keeps its edge; only the fabricated project CALLS edge is dropped. */
static bool emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const cbm_gbuf_node_t *target, const CBMCall *call,
const cbm_resolution_t *res, const char *module_qn,
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
const char **imp_keys, const char **imp_vals, int imp_count,
bool suppress_plain_calls, const char *route_mount,
CBMLanguage lang) {
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
const char *arg = call->first_string_arg;
/* Also detect route registration by callee name suffix alone (handles unresolved
* local variables like app.include_router where QN resolution fails). */
if (svc == CBM_SVC_NONE && cbm_service_pattern_route_method(call->callee_name) != NULL) {
svc = CBM_SVC_ROUTE_REG;
}
/* Detect gRPC stub method calls by resolved QN.
* Go pattern: pb.NewCartServiceClient(conn).GetCart(ctx, req)
* Tree-sitter extracts GetCart as the callee, which resolves to the
* generated pb interface method (QN contains "ServiceClient"). */
if (svc == CBM_SVC_NONE && res->qualified_name) {
if (strstr(res->qualified_name, "ServiceClient") != NULL ||
strstr(res->qualified_name, "ServiceGrpc") != NULL ||
strstr(res->qualified_name, "Servicer") != NULL) {
svc = CBM_SVC_GRPC;
}
}
if (svc == CBM_SVC_ROUTE_REG) {
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
route_mount, lang);
}
/* No path found — fall through to normal CALLS edge */
}
bool has_url = (arg && arg[0] != '\0' && (arg[0] == '/' || strstr(arg, "://") != NULL));
bool has_topic = (arg && arg[0] != '\0' && svc == CBM_SVC_ASYNC && strlen(arg) > PP_ESC_SPACE);
/* Set when this call already has its HTTP_CALLS edge from the service
* patterns (typed with the verb its callee names). */
bool http_edge_emitted = false;
bool calls_emitted = false;
if ((svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) && (has_url || has_topic)) {
emit_http_async_service_edge(gbuf, source, call, res, svc, arg);
http_edge_emitted = svc == CBM_SVC_HTTP;
} else if (svc == CBM_SVC_GRPC) {
emit_grpc_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_GRAPHQL) {
emit_graphql_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_TRPC) {
emit_trpc_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_CONFIG) {
emit_config_edge(gbuf, source, target, call, res, arg);
} else if (!suppress_plain_calls) {
calls_emitted = emit_normal_calls_edge(gbuf, source, target, call, res);
}
/* The arg-URL heuristic is for calls no service pattern knows (a local
* fetch wrapper). On a classified HTTP call it only minted a method-less
* __route__ANY__ twin of the typed edge, which cross-repo matching then
* bound to server handlers of any method. */
if (!http_edge_emitted) {
detect_url_in_args(gbuf, source, call, svc, pp_client_navigation_lang(lang));
}
return calls_emitted;
}
/* The #725 guard refuses a suffix_match binding across a language boundary,
* which leaves the callee unresolved. An unresolved callee the route
* classifier recognises is still a registration: `Route::get('/x', ...)`
* beside a JS `get`, `app.get('/x', h)` beside a Python `get`. Emit exactly
* the Route + CALLS + HANDLES that the unresolved-callee (callee_suffix)
* fallback in resolve_file_calls emits for it, classified by callee name the
* same way emit_service_edge classifies that fallback, and nothing else: no
* CALLS edge to the refused target and no URL-argument scan. Dropping the
* whole call lost every GET registration in a mixed-language repo while POST
* (no `post` to collide with) survived. Mirrors pass_calls.c. */
static bool emit_xlang_refused_route(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const CBMCall *call, const char *module_qn,
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
const char **imp_keys, const char **imp_vals, int imp_count,
const char *route_mount, CBMLanguage lang) {
if (cbm_service_pattern_route_method(call->callee_name) == NULL) {
return false;
}
cbm_svc_kind_t svc = cbm_service_pattern_match(call->callee_name);
if (svc != CBM_SVC_NONE && svc != CBM_SVC_ROUTE_REG) {
return false;
}
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
route_mount, lang);
}
return false;
}
/* Find the source node for an edge: enclosing function or file node. */
/* This worker's last file-node answer. The graph buffer is read-only for the
* whole of phase 4 and a file's rel_path pointer is stable within it, so the
* pair identifies the answer; resolve_worker clears it at both ends so a later
* phase can never match a recycled address. Computing the file QN and looking
* it up ran for every call, usage, throw and read/write whose enclosing
* function is not a graph node of its own. */
static CBM_TLS const cbm_gbuf_t *tl_file_node_gbuf;
static CBM_TLS const char *tl_file_node_rel;
static CBM_TLS const cbm_gbuf_node_t *tl_file_node;
static void file_node_cache_clear(void) {
tl_file_node_gbuf = NULL;
tl_file_node_rel = NULL;
tl_file_node = NULL;
}
const cbm_gbuf_node_t *cbm_pipeline_file_node(const cbm_gbuf_t *gbuf, const char *project,
const char *rel) {
char *file_qn = cbm_pipeline_fqn_compute(project, rel, "__file__");
const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(gbuf, file_qn);
free(file_qn);
return node;
}
static const cbm_gbuf_node_t *file_node_for(const cbm_gbuf_t *gbuf, const char *project,
const char *rel) {
if (tl_file_node_gbuf == gbuf && tl_file_node_rel == rel) {
return tl_file_node;
}
const cbm_gbuf_node_t *node = cbm_pipeline_file_node(gbuf, project, rel);
tl_file_node_gbuf = gbuf;
tl_file_node_rel = rel;
tl_file_node = node;
return node;
}
static const cbm_gbuf_node_t *find_source_node(const cbm_gbuf_t *gbuf, const char *project,
const char *rel, const char *enclosing_qn) {
const cbm_gbuf_node_t *src = NULL;
if (enclosing_qn) {
src = cbm_gbuf_find_by_qn(gbuf, enclosing_qn);
/* A class-level reference in a directory-module language carries the
* DIRECTORY module QN, which hits the shared Folder/Project node —
* attribute to this file's File node instead (#787). */
if (cbm_pipeline_node_is_dir_container(src)) {
src = NULL;
}
}
if (!src) {
src = file_node_for(gbuf, project, rel);
}
return src;
}
/* Field type hint resolution for obj.Method() with multiple candidates.
* Strips C# field prefixes (_ / m_), capitalizes to get type name, and
* checks if TypeName.Method or ITypeName.Method exists among candidates. */
static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const char *callee_name,
int64_t source_id) {
if (!res->qualified_name || res->candidate_count <= SKIP_ONE) {
return;
}
const char *dot = strchr(callee_name, '.');
if (!dot) {
return;
}
size_t plen = (size_t)(dot - callee_name);
char obj_name[CBM_SZ_256];
if (plen >= sizeof(obj_name)) {
return;
}
memcpy(obj_name, callee_name, plen);
obj_name[plen] = '\0';
const char *type_hint = obj_name;
if (type_hint[0] == '_') {
type_hint++;
}
if (type_hint[0] == 'm' && type_hint[SKIP_ONE] == '_') {
type_hint += PP_CSHARP_M_PREFIX_LEN;
}
char type_name[CBM_SZ_256];
snprintf(type_name, sizeof(type_name), "%s", type_hint);
if (type_name[0] >= 'a' && type_name[0] <= 'z') {
type_name[0] -= ('a' - 'A');
}
char iface_name[CBM_SZ_256];
snprintf(iface_name, sizeof(iface_name), "I%s", type_name);
const char *method = dot + SKIP_ONE;
const char **cands = NULL;
int cand_count = 0;
cbm_registry_find_by_name(rc->registry, method, &cands, &cand_count);
for (int ci = 0; ci < cand_count; ci++) {
if (strstr(cands[ci], type_name) || strstr(cands[ci], iface_name)) {
const cbm_gbuf_node_t *better = cbm_gbuf_find_by_qn(rc->main_gbuf, cands[ci]);
if (better && better->id != source_id) {
res->qualified_name = cands[ci];
res->confidence = PP_FIELD_HINT_CONF;
res->strategy = "field_type_hint";
return;
}
}
}
}
/* Free a strdup'd key stored in the per-file lsp_idx hash table. */
static void lsp_idx_free_key(const char *key, void *value, void *ud) {
(void)value;
(void)ud;
free((char *)key);
}
/* A hash key may have multiple semantic rows. Keep a permanent tombstone for
* distinct targets so O(1) lookup cannot turn ambiguity into a confidence
* contest; duplicate rows for the same target may still keep the best score. */
static CBMResolvedCall *const LSP_IDX_AMBIGUOUS = (CBMResolvedCall *)(uintptr_t)1;
static bool lsp_idx_keep_best(CBMHashTable *index, const char *key, CBMResolvedCall *candidate,
const cbm_gbuf_t *gbuf, const char *project_name,
bool allow_tail_match) {
if (!index || !key || !candidate) {
return false;
}
CBMResolvedCall *existing = (CBMResolvedCall *)cbm_ht_get(index, key);
if (!existing) {
char *owned_key = strdup(key);
if (owned_key) {
cbm_ht_set(index, owned_key, candidate);
if (cbm_ht_get(index, key) == candidate) {
return true;
}
free(owned_key);
}
return false;
}
if (existing == LSP_IDX_AMBIGUOUS) {
return true;
}
if (!cbm_pipeline_invocation_targets_equal(existing->callee_qn, candidate->callee_qn, gbuf,
project_name, allow_tail_match)) {
const char *stored_key = cbm_ht_get_key(index, key);
if (stored_key) {
cbm_ht_set(index, stored_key, LSP_IDX_AMBIGUOUS);
}
return cbm_ht_get(index, key) != NULL;
}
if (candidate->confidence > existing->confidence) {
const char *stored_key = cbm_ht_get_key(index, key);
if (stored_key) {
cbm_ht_set(index, stored_key, candidate);
}
}
return cbm_ht_get(index, key) != NULL;
}
/* Caller QNs are user-controlled graph identifiers and can legitimately exceed
* the old 1 KiB stack buffer. Build occurrence keys at their exact length so an
* exact semantic row can never disappear merely because its caller is long. */
static char *lsp_idx_key(const char *caller_qn, const char *leaf, bool exact_site,
uint32_t site_start_byte, uint32_t site_end_byte,
CBMSourceOrigin source_origin) {
if (!caller_qn || !leaf) {
return NULL;
}
size_t caller_len = strlen(caller_qn);
size_t leaf_len = strlen(leaf);
size_t suffix_len = exact_site ? 48U : 16U;
if (caller_len > SIZE_MAX - leaf_len - suffix_len - 2U) {
return NULL;
}
size_t cap = caller_len + 1U + leaf_len + suffix_len + 1U;
char *key = (char *)malloc(cap);
if (!key) {
return NULL;
}
int written = exact_site
? snprintf(key, cap, "%s|%s|%u:%u|%u", caller_qn, leaf, site_start_byte,
site_end_byte, (unsigned)source_origin)
: snprintf(key, cap, "%s|%s|%u", caller_qn, leaf, (unsigned)source_origin);
if (written <= 0 || (size_t)written >= cap) {
free(key);
return NULL;
}
return key;
}
static bool lsp_idx_insert_leaf(CBMHashTable *index, CBMResolvedCall *candidate, const char *leaf,
bool exact_site, const cbm_gbuf_t *gbuf, const char *project_name,
bool allow_tail_match) {
if (!index || !candidate || !candidate->caller_qn || !leaf || !leaf[0]) {
return false;
}
char *key = lsp_idx_key(candidate->caller_qn, leaf, exact_site, candidate->site_start_byte,
candidate->site_end_byte, candidate->source_origin);
if (!key) {
return false;
}
bool inserted = lsp_idx_keep_best(index, key, candidate, gbuf, project_name, allow_tail_match);
free(key);
return inserted;
}
static const CBMResolvedCall *lsp_idx_lookup(const CBMHashTable *index, const CBMCall *call,
bool exact_site, bool *key_built, bool *ambiguous) {
if (key_built) {
*key_built = false;
}
if (ambiguous) {
*ambiguous = false;
}
if (!index || !call || !call->enclosing_func_qn || !call->callee_name) {
return NULL;
}
const char *leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
if (!leaf || !leaf[0]) {
return NULL;
}
char *key = lsp_idx_key(call->enclosing_func_qn, leaf, exact_site, call->site_start_byte,
call->site_end_byte, call->source_origin);
if (!key) {
return NULL;
}
if (key_built) {
*key_built = true;
}
const CBMResolvedCall *candidate = (const CBMResolvedCall *)cbm_ht_get(index, key);
free(key);
if (candidate == LSP_IDX_AMBIGUOUS) {
if (ambiguous) {
*ambiguous = true;
}
return NULL;
}
if (!candidate || candidate->kind != CBM_RESOLVED_INVOCATION || !candidate->caller_qn ||
strcmp(candidate->caller_qn, call->enclosing_func_qn) != 0 ||
candidate->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
return NULL;
}
int site_rank = cbm_pipeline_invocation_site_rank(candidate, call);
int expected_rank = exact_site ? 2 : 1;
return site_rank == expected_rank &&
cbm_pipeline_invocation_leaf_matches(candidate, call, site_rank)
? candidate
: NULL;
}
static CBMCallEvidence *resolve_evidence_at(CBMCallEvidence *evidence, int index,
CBMCallEvidence *fallback) {
if (evidence) {
return &evidence[index];
}
return fallback;
}
static CBMCallEvidence *resolve_evidence_new(const resolve_ctx_t *rc, const CBMFileResult *result) {
if (result->calls.count == 0) {
return NULL;
}
CBMCallEvidence *evidence =
cbm_calloc(CBM_MEM_CLASS_RESOLVE, (size_t)result->calls.count * sizeof(*evidence));
if (!evidence) {
cbm_pipeline_mark_unresolved_capture_failed(rc->pctx ? rc->pctx->pipeline : NULL);
}
return evidence;
}
/* Resolve calls for one file and emit CALLS/HTTP_CALLS/ASYNC_CALLS edges. */
static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang,
const cbm_php_vendor_names_t *php_vendor,
CBMCallEvidence *evidence) {
/* Framework mount of this file's routes (Laravel 11+ withRouting, #1146). */
char route_mount[CBM_SZ_128];
cbm_laravel_file_route_mount(rc->repo_path, rel, lang, result, route_mount,
sizeof(route_mount));
/* Two occurrence-aware indexes preserve the authoritative matcher's
* primary ordering without restoring its O(calls × resolutions) scan:
* exact caller+leaf+span first, then the legacy caller+leaf fallback.
* Repeated same-leaf calls therefore remain O(1), and a high-confidence
* 0:0 record can never hide an exact source occurrence. */
CBMHashTable *lsp_exact_idx = NULL;
CBMHashTable *lsp_legacy_idx = NULL;
bool lsp_exact_idx_complete = true;
bool lsp_legacy_idx_complete = true;
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
/* Names this file's imports bind onto external libraries, built on the
* first unresolved call that asks (see pass_calls.c). */
cbm_external_import_index_t ext_imports = {0};
if (result->calls.count > 0 && result->resolved_calls.count > 0) {
uint32_t capacity = (uint32_t)result->resolved_calls.count * 2u + 16u;
lsp_exact_idx = cbm_ht_create(capacity);
lsp_legacy_idx = cbm_ht_create(capacity);
if (!lsp_exact_idx) {
lsp_exact_idx_complete = false;
}
if (!lsp_legacy_idx) {
lsp_legacy_idx_complete = false;
}
if (lsp_exact_idx || lsp_legacy_idx) {
for (int i = 0; i < result->resolved_calls.count; i++) {
CBMResolvedCall *rc_e = &result->resolved_calls.items[i];
if (rc_e->kind != CBM_RESOLVED_INVOCATION || !rc_e->caller_qn || !rc_e->callee_qn ||
rc_e->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
bool exact_site =
cbm_pipeline_source_site_present(rc_e->site_start_byte, rc_e->site_end_byte);
bool legacy_site =
cbm_pipeline_source_site_legacy(rc_e->site_start_byte, rc_e->site_end_byte);
if (!exact_site && !legacy_site) {
continue;
}
CBMHashTable *index = exact_site ? lsp_exact_idx : lsp_legacy_idx;
bool inserted =
lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->callee_qn),
exact_site, rc->main_gbuf, rc->project_name, allow_tail);
if (!inserted) {
if (exact_site) {
lsp_exact_idx_complete = false;
} else {
lsp_legacy_idx_complete = false;
}
}
if (rc_e->reason && cbm_pipeline_invocation_reason_join_strategy(rc_e->strategy)) {
inserted = lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->reason),
exact_site, rc->main_gbuf, rc->project_name,
allow_tail);
if (!inserted) {
if (exact_site) {
lsp_exact_idx_complete = false;
} else {
lsp_legacy_idx_complete = false;
}
}
}
if (exact_site && rc_e->strategy && strcmp(rc_e->strategy, "lsp_destructor") == 0 &&
!lsp_idx_insert_leaf(index, rc_e, "~", true, rc->main_gbuf, rc->project_name,
allow_tail)) {
lsp_exact_idx_complete = false;
}
}
}
}
/* Field-owner rows, indexed on the first C member call that needs them. */
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_built = false;
bool field_index_ready = false;
for (int c = 0; c < result->calls.count; c++) {
const CBMCall *call = &result->calls.items[c];
CBMCallEvidence fallback = {0};
CBMCallEvidence *site_evidence = resolve_evidence_at(evidence, c, &fallback);
if (!call->callee_name) {
continue;
}
uint64_t _rc_t0 = extract_now_ns();
const cbm_gbuf_node_t *source_node =
find_source_node(rc->main_gbuf, rc->project_name, rel, call->enclosing_func_qn);
atomic_fetch_add_explicit(&rc->time_ns_rc_source, extract_now_ns() - _rc_t0,
memory_order_relaxed);
if (!source_node) {
continue;
}
/* LSP-resolved calls take precedence over registry textual matching.
* Same helper + same CBM_LSP_CONFIDENCE_FLOOR as the sequential
* pipeline (pass_calls.c) — both paths must admit the same set of
* LSP overrides so a project doesn't get different attributions
* depending on whether parallel mode kicked in. Unique-tail
* fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
cbm_resolution_t res = {0};
const CBMResolvedCall *lsp = NULL;
bool exact_key_built = true;
bool exact_key_ambiguous = false;
bool legacy_key_built = true;
_rc_t0 = extract_now_ns();
if (cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte)) {
lsp = lsp_idx_lookup(lsp_exact_idx, call, true, &exact_key_built, &exact_key_ambiguous);
}
bool exact_index_authoritative = lsp_exact_idx_complete && exact_key_built;
if (!lsp && !exact_index_authoritative) {
/* An incomplete exact index must fail closed: consult the
* authoritative matcher before a legacy row can win. */
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
}
if (!lsp && !call->requires_lsp_resolution && exact_index_authoritative &&
!exact_key_ambiguous) {
lsp = lsp_idx_lookup(lsp_legacy_idx, call, false, &legacy_key_built, NULL);
}
if (!lsp && (!lsp_legacy_idx_complete || !legacy_key_built || allow_tail ||
call->requires_lsp_resolution)) {
/* Fallback to the linear scan for edge cases the index may
* miss (e.g. callee_name that wasn't the registered short
* name). Keeps semantics identical. */
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
}
atomic_fetch_add_explicit(&rc->time_ns_rc_lsp_lookup, extract_now_ns() - _rc_t0,
memory_order_relaxed);
_rc_t0 = extract_now_ns();
const cbm_gbuf_node_t *lsp_target = NULL;
if (lsp) {
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
* the gbuf even when the cross-file fallback had to prefix the
* project name. */
bool exact_external_target = call->requires_lsp_resolution &&
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
lsp_target = exact_external_target
? cbm_pipeline_lsp_target_node_strict(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail);
if (lsp_target) {
res.qualified_name = lsp_target->qualified_name;
res.strategy = lsp->strategy ? lsp->strategy : "lsp_override";
res.confidence = (double)lsp->confidence;
res.candidate_count = 1;
ws->lsp_overrides++;
}
}
/* #1085: fall back to the registry resolver whenever the LSP did not
* yield a gbuf-resolvable target — whether no LSP resolution existed,
* OR the LSP was confident but its callee_qn isn't a node in the gbuf
* (the JSX-via-tsconfig-alias case: the TS LSP resolves the element
* ref to an alias-path QN that never matches a def node, so lsp_target
* is NULL). The old `else` ran the registry ONLY when lsp was null, so
* an LSP-with-unresolvable-target dropped the edge outright — silently
* losing every alias-imported JSX component edge on the parallel path
* (~21% of a Next.js call graph) while the sequential pass, which falls
* THROUGH to the registry here, kept them. This restores seq/parallel
* parity via the import_map / unique_name resolution. Synthetic
* semantic candidates are deliberately excluded: they require an
* exact LSP target and must fail closed rather than accepting a textual
* registry match. */
/* #2053: a Rust call the LSP placed on an EXTERNAL symbol (std's
* Path::join, a seeded crate API) is resolved — it just has no graph
* node. The textual registry would bind it to a same-named project
* method instead, so it must not run for such a row. Mirrors
* pass_calls.c; the service fallbacks below still see the call. */
bool php_vendor_call = cbm_pipeline_php_vendor_call(php_vendor, result, call) ||
(lsp && !lsp_target && cbm_pipeline_lsp_external_receiver(lsp));
bool rust_external = lsp && cbm_pipeline_rust_external_target(
lang, lsp->strategy, lsp->callee_qn, rc->project_name);
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution &&
!rust_external && !php_vendor_call) {
/* Python LEGB fallback first (MUST match pass_calls.c). */
res = cbm_registry_resolve_call_scoped(rc->registry, call->callee_name, module_qn,
imp_keys, imp_vals, imp_count, lang,
call->enclosing_func_qn);
}
/* `p->open(fd)`: the registry does not split a callee on the arrow.
* The object's type decides, as for the dot call handled further
* down. Mirrors pass_calls.c. */
bool arrow_bound = false;
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution &&
cbm_c_arrow_member_call(lang, call->callee_name)) {
if (!field_index_built) {
field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
field_index_built = true;
}
arrow_bound =
cbm_pipeline_c_member_call_resolve(
&result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf,
rc->registry, rc->project_name, call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res) != NULL;
}
atomic_fetch_add_explicit(&rc->time_ns_rc_resolve, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* A synthetic semantic candidate is an invocation only when the LSP
* resolved it to a concrete graph node. Never let registry, field-name,
* route, or service heuristics manufacture a target for it. */
if (call->requires_lsp_resolution && !lsp_target) {
continue;
}
_rc_t0 = extract_now_ns();
/* Not for Go (F-G9): the hint matches the capitalized receiver
* VARIABLE name as a substring of a candidate QN (`t.Run` -> any
* `...T...Run`), with no type evidence. The sequential resolver has no
* such step, so for Go it produced parallel-only edges (par/seq CALLS
* diverged), and the Go selector guard could not see them as the weak
* pick they are. SCIP precision of Go field_type_hint: 0.016. */
if (lang != CBM_LANG_GO) {
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
}
/* Cross-language veto — the same predicate as pass_calls.c, applied
* AFTER the field-type hint so the hint may still re-pick a compatible
* candidate first, and BEFORE the empty-resolution fallbacks below so a
* vetoed call still reaches route/HTTP classification. */
if (cbm_registry_name_guess_vetoed(rc->registry, lang, &res)) {
res = (cbm_resolution_t){0};
}
atomic_fetch_add_explicit(&rc->time_ns_rc_hint, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* Perl call-graph noise guard (#476), mirroring the sequential pass
* (pass_calls.c). Perl has no LSP resolver; for builtins (push/shift/
* keys/...) and method calls ($obj->m, unresolved receiver), suppress
* only WEAK cross-file short-name matches and keep the high-confidence
* same_module / import_map strategies so a genuine same-file or
* imported call to a builtin-named sub still resolves. Placed after the
* field-type hint so a hint cannot re-introduce a suppressed edge.
* Gated to Perl — other languages are unaffected. */
if (cbm_perl_suppress_generic_match(lang == CBM_LANG_PERL, call->is_method,
call->callee_name, res.strategy)) {
continue;
}
/* Dynamic-language weak-member suppression (#592/#606/#1276). The
* receiver-aware guard must NOT drop this call here: doing so would also
* skip the #523 callee-name service bypass below, emit_service_edge's
* route/gRPC/config branches, and its detect_url_in_args
* (which classifies verb-suffix HTTP clients like api.patch('/x')).
* Instead, defer to the emit path and suppress ONLY the plain-CALLS
* fall-through (emit_normal_calls_edge), so every service edge stays
* main-identical by construction. res.strategy may carry an lsp_* value
* here (LSP-resolved calls keep res through this point); the helper's
* EXPLICIT drop-list leaves lsp_ts_method / lsp_cross untouched. See
* #606 direction.
*
* This language set MUST match the one in pass_calls.c exactly — see the
* note there. ArkTS belongs to the JS/TS family (#1842). Go (#1906)
* composes via its own predicate (different drop-list — see
* cbm_go_suppress_weak_method_match), mirrored in pass_calls.c. */
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS ||
/* embedded-script hosts — see pass_calls.c */
lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
/* Bare-call local-binding suppression — see the note in pass_calls.c.
* This gate MUST stay identical to the one there. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
/* The member guard's one exemption — MUST match pass_calls.c exactly. */
/* #1355: same guard as pass_calls.c, and joined to drop_plain_call for
* the same reason — a `continue` here would also skip route/HTTP/CONFIG
* classification, and a static-import route registration is a bare call
* bound by a package specifier. Suppress only the plain-CALLS
* fall-through so every Route node stays main-identical. */
bool drop_plain_call =
(cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) &&
!cbm_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
call->receiver_is_self_attribute,
call->callee_name, res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence) ||
/* Import-binding suppression (#2127) — see pass_calls.c; this gate
* MUST stay identical to the one there. */
(lang == CBM_LANG_PYTHON &&
cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
cbm_python_import_binding_contradicts(&result->imports, call->callee_name,
res.qualified_name, rc->main_gbuf,
rc->project_name, rel)) ||
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, &result->imports,
imp_keys, imp_count, cbm_pipeline_get_pkgmap(),
cbm_pipeline_get_nsmap()) ||
/* MUST match pass_calls.c exactly. */
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
cbm_pipeline_php_receiver_type_unindexed(&result->resolved_calls, call, rc->registry));
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
* it to a spurious builtin short-name match (`requests.get` ->
* `builtins.dict.get` via "get"), which is non-empty and not an HTTP
* pattern, so the resolved-QN service checks below miss it and the call
* is dropped. Detect it on the callee_name FIRST so the HTTP_CALLS/
* ASYNC_CALLS edge is emitted regardless (target is a synthesized route
* node, not the unindexed library). Mirrors pass_calls.c. (#523)
*
* First: a call on an axios.create() instance (#1916) — decided by the
* receiver's binding, before the #523 spelling check and the
* route-registration suffix fallback. MUST match pass_calls.c. */
char client_url[CBM_SZ_512];
if (cbm_pipeline_http_client_call_url(rc->main_gbuf, rc->project_name, rel, result,
imp_keys, imp_vals, imp_count, call, client_url,
sizeof(client_url))) {
cbm_resolution_t svc_res = {.qualified_name = call->callee_name,
.confidence = PP_HALF_CONF,
.strategy = "http_client_instance"};
emit_http_async_service_edge(ws->local_edge_buf, source_node, call, &svc_res,
CBM_SVC_HTTP, client_url);
continue;
}
/* A PHP call on a proven vendor class is classified by that class
* (#1186): it skipped the registry above, so the route-verb fallback
* would otherwise mint a route registration for
* `GuzzleClient::get('/x')`. MUST match pass_calls.c. */
cbm_svc_kind_t csvc;
const char *svc_name = cbm_pipeline_service_name(call, lsp_target ? NULL : lsp, &csvc);
if (csvc == CBM_SVC_HTTP || csvc == CBM_SVC_ASYNC) {
const char *cu = call->first_string_arg;
bool chas_url = cu && cu[0] != '\0' &&
(cu[0] == '/' || strstr(cu, "://") != NULL ||
(csvc == CBM_SVC_ASYNC && strlen(cu) > PP_ESC_SPACE));
if (chas_url) {
cbm_resolution_t svc_res = {.qualified_name = svc_name,
.confidence = PP_HALF_CONF,
.strategy = "service_pattern"};
site_evidence->calls_emitted |=
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &svc_res,
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
imp_count, false, route_mount, lang);
continue;
}
}
/* A call that starts another program (subprocess.run, exec.Command,
* posix_spawn): a SPAWNS edge to its Process node instead of a CALLS
* edge. `res` holds the LSP answer when there is one, as the
* sequential pass asks inside its LSP branch. MUST match pass_calls.c. */
cbm_pipeline_spawn_t spawn;
if (cbm_pipeline_spawn_site(rc->main_gbuf, lang, call, &result->imports, &res, &spawn)) {
cbm_pipeline_emit_spawn(ws->local_edge_buf, source_node, call, &spawn);
continue;
}
if (!res.qualified_name || res.qualified_name[0] == '\0') {
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
cbm_resolution_t fake_res = {.qualified_name = call->callee_name,
.confidence = PP_HALF_CONF,
.strategy = "callee_suffix"};
/* The target here is the SOURCE itself (nothing resolved), so
* the plain-CALLS fall-through for a route-verb call without a
* route path (`cache.get(key)`) would be a self-loop; that holds
* for #2053's LSP-external Rust calls too. Suppress it always;
* route/HTTP/service classification is unchanged. */
site_evidence->calls_emitted |=
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &fake_res,
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
imp_count, true, route_mount, lang);
} else if (cbm_service_pattern_is_global_fetch(call->callee_name)) {
/* Native `fetch()` (#856): only the global API once resolution
* has failed to find a local/imported `fetch`. Call the low-level
* emitter directly — emit_service_edge re-derives its own kind
* from res->qualified_name via cbm_service_pattern_match, which
* "fetch" deliberately never matches (mirrors pass_calls.c). */
const char *u = call->first_string_arg;
if (u && u[0] != '\0' && (u[0] == '/' || strstr(u, "://") != NULL)) {
cbm_resolution_t fake_res = {.qualified_name = call->callee_name,
.confidence = PP_HALF_CONF,
.strategy = "service_pattern"};
emit_http_async_service_edge(ws->local_edge_buf, source_node, call, &fake_res,
CBM_SVC_HTTP, u);
}
} else {
/* A callee an import binds to an external client library
* (`send` under `from paho.mqtt import publish as send`):
* spelled through its import, then classified. MUST match
* pass_calls.c. */
char spelled[CBM_SZ_512];
cbm_svc_kind_t isvc = cbm_pipeline_external_import_service(
&ext_imports, rc->main_gbuf, rc->project_name, rel, lang, &result->imports,
call, spelled, sizeof(spelled));
if (isvc != CBM_SVC_NONE) {
CBMCall through = *call;
through.callee_name = spelled;
cbm_resolution_t imp_res = {.qualified_name = spelled,
.confidence = PP_HALF_CONF,
.strategy = "import_binding"};
emit_http_async_service_edge(ws->local_edge_buf, source_node, &through,
&imp_res, isvc, through.first_string_arg);
}
}
continue;
}
/* Reuse lsp_target as target_node when LSP resolved — avoids a
* second cbm_gbuf_find_by_qn lookup. try_field_type_hint may have
* upgraded res.qualified_name to a different candidate, in which
* case we must re-resolve. */
_rc_t0 = extract_now_ns();
const cbm_gbuf_node_t *target_node;
if (lsp_target && res.qualified_name == lsp_target->qualified_name) {
target_node = lsp_target;
} else {
target_node = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
}
atomic_fetch_add_explicit(&rc->time_ns_rc_target, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* A call that resolved onto a struct Field: refused across languages
* and for a bare C call; a C/C++ member call takes the Field its
* object's type names. Mirrors the sequential twin (pass_calls.c). */
if (target_node && source_node->id != target_node->id) {
cbm_field_call_policy_t field_policy = cbm_call_onto_field_policy(
lang, call->callee_name, target_node->label, target_node->file_path);
if (field_policy == CBM_FIELD_CALL_DROP) {
continue;
}
if (field_policy == CBM_FIELD_CALL_BY_OWNER && !arrow_bound) {
if (!field_index_built) {
field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
field_index_built = true;
}
target_node = cbm_pipeline_c_member_call_resolve(
&result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf,
rc->registry, rc->project_name, call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res);
if (!target_node || source_node->id == target_node->id) {
continue;
}
}
}
if (target_node && source_node->id != target_node->id &&
cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
/* #725: same guard as pass_calls.c — do not emit a suffix_match
* CALLS edge across a language boundary. A route registration
* behind the refused binding still gets its Route. */
site_evidence->calls_emitted |= emit_xlang_refused_route(
ws->local_edge_buf, source_node, call, module_qn, rc->registry, rc->main_gbuf,
imp_keys, imp_vals, imp_count, route_mount, lang);
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
* target is a synthesized route node, not the library — emit them
* anyway so cross-repo matching has an HTTP_CALLS edge to work with
* (#523). Mirrors the sequential resolve_single_call bypass. */
cbm_svc_kind_t psvc = cbm_service_pattern_match(res.qualified_name);
if ((psvc == CBM_SVC_HTTP || psvc == CBM_SVC_ASYNC) && !target_node) {
const char *u = call->first_string_arg;
bool url_or_topic = u && u[0] != '\0' &&
(u[0] == '/' || strstr(u, "://") != NULL ||
(psvc == CBM_SVC_ASYNC && strlen(u) > PP_ESC_SPACE));
if (url_or_topic) {
site_evidence->calls_emitted |= emit_service_edge(
ws->local_edge_buf, source_node, NULL, call, &res, module_qn, rc->registry,
rc->main_gbuf, imp_keys, imp_vals, imp_count, false, route_mount, lang);
ws->calls_resolved++;
}
}
continue;
}
site_evidence->candidate_qn = target_node->qualified_name;
_rc_t0 = extract_now_ns();
site_evidence->calls_emitted |= emit_service_edge(
ws->local_edge_buf, source_node, target_node, call, &res, module_qn, rc->registry,
rc->main_gbuf, imp_keys, imp_vals, imp_count, drop_plain_call, route_mount, lang);
atomic_fetch_add_explicit(&rc->time_ns_rc_emit, extract_now_ns() - _rc_t0,
memory_order_relaxed);
ws->calls_resolved++;
}
if (lsp_exact_idx) {
cbm_ht_foreach(lsp_exact_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_exact_idx);
}
if (lsp_legacy_idx) {
cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_legacy_idx);
}
cbm_pipeline_external_import_index_free(&ext_imports);
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* Resolve usages for one file. */
static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
CBMFileResult *result, const char *rel, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count,
CBMLanguage lang, const cbm_php_vendor_names_t *php_vendor) {
cbm_pipeline_lsp_reference_index_t reference_index = {0};
bool reference_index_ready =
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int u = 0; u < result->usages.count; u++) {
CBMUsage *usage = &result->usages.items[u];
if (!usage->ref_name) {
continue;
}
const cbm_gbuf_node_t *src =
find_source_node(rc->main_gbuf, rc->project_name, rel, usage->enclosing_func_qn);
if (!src) {
continue;
}
/* A C member name binds through the type of its object, published by
* the C LSP as field-owner rows, and through nothing else. Must mirror
* the sequential twin (pass_usages.c) exactly. */
if (usage->kind == CBM_USAGE_VALUE &&
cbm_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel),
usage->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
usage->enclosing_func_qn, usage->ref_name);
char member_esc[CBM_SZ_256];
cbm_json_escape(member_esc, sizeof(member_esc), usage->ref_name);
char member_props[CBM_SZ_512];
snprintf(member_props, sizeof(member_props), "{\"callee\":\"%s\"}", member_esc);
const CBMResolvedCall *owner;
bool typed = false;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
typed = true;
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, "USAGE", member_props);
ws->usages_resolved++;
}
if (!typed) {
/* The LSP could not type the object: only a member name the
* project holds exactly once may still bind. */
const char *only = cbm_registry_unique_field_qn(rc->registry, usage->ref_name);
const cbm_gbuf_node_t *field =
only ? cbm_gbuf_find_by_qn(rc->main_gbuf, only) : NULL;
if (field && field->id != src->id &&
!cbm_suppress_cross_language_ref(lang, field->file_path)) {
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, "USAGE",
member_props);
ws->usages_resolved++;
}
}
continue;
}
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
bool property_read = false;
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
&result->resolved_calls, reference_index_ready ? &reference_index : NULL, usage,
allow_tail, rc->main_gbuf, rc->project_name);
if (semantic_reference &&
!cbm_pipeline_usage_allows_semantic_reference(usage, semantic_reference)) {
semantic_reference = NULL;
}
if (semantic_reference) {
tgt = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
semantic_reference->callee_qn, allow_tail);
if (!cbm_pipeline_reference_target_admissible(semantic_reference, tgt)) {
/* a property read: the name fallback below may still bind the
* property's own node, never a callable */
tgt = NULL;
semantic_reference = NULL;
property_read = true;
}
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
}
}
if (!tgt) {
/* Exact semantic ownership beats textual name fallback even when
* the semantic target is not materialized in this graph. */
if (semantic_reference) {
continue;
}
/* #1186: a PHP name imported from a vendor namespace is not the
* same-named project symbol. Mirrors pass_usages.c. */
if (cbm_pipeline_php_vendor_usage(php_vendor, result, usage)) {
continue;
}
/* SQL usages are FROM/JOIN lineage refs and may bind Table/View
* targets (cbm_registry_resolve_lineage); every other language
* resolves through the default variant, whose central relation
* veto keeps same-named code identifiers out of the lineage layer.
* Must mirror the sequential twin (pass_usages.c) exactly. */
cbm_resolution_t res =
(lang == CBM_LANG_SQL)
? cbm_registry_resolve_lineage(rc->registry, usage->ref_name, module_qn,
imp_keys, imp_vals, imp_count)
: cbm_registry_resolve_lang(rc->registry, usage->ref_name, module_qn, imp_keys,
imp_vals, imp_count, lang);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
}
if (!cbm_pipeline_reference_candidate_fallback_allowed(lang, usage, res.strategy,
imp_keys, imp_count)) {
continue;
}
/* a plain value reference names the field twin of a same-named
* method; a callable-reference candidate keeps the method */
bool value_ref = usage->kind == CBM_USAGE_VALUE && !usage->may_be_call_reference;
tgt = cbm_gbuf_find_by_qn(
rc->main_gbuf, value_ref
? cbm_registry_value_target(rc->registry, res.qualified_name)
: res.qualified_name);
/* #1928: the registry fallback is a bare-name guess — never let it
* bind a reference across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct
* field; the member half of a selector may. */
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
}
if (property_read && cbm_pipeline_node_is_callable_target(tgt)) {
continue;
}
}
if (!tgt || src->id == tgt->id) {
continue;
}
/* 512, matching the sequential twin in pass_usages.c. esc_ref holds up
* to 255 bytes and the {"callee":"..."} wrapper adds 13, so a 256-byte
* uprops truncates a long callable identifier mid-string -- cutting the
* closing quote-brace and persisting MALFORMED JSON. The two paths must
* also agree: the same repo indexed in parallel and sequentially has to
* produce byte-identical edge properties. */
char uprops[CBM_SZ_512];
char esc_ref[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
const char *edge_type = precise_call_reference ? "CALL_REFERENCE" : "USAGE";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, edge_type, uprops);
ws->usages_resolved++;
}
cbm_pipeline_lsp_reference_index_free(&reference_index);
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* Resolve throws/raises for one file. */
static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws,
CBMFileResult *result, const char *rel, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count,
CBMLanguage lang) {
for (int t = 0; t < result->throws.count; t++) {
CBMThrow *thr = &result->throws.items[t];
if (!thr->exception_name || !thr->enclosing_func_qn) {
continue;
}
/* find_source_node falls back to the per-file File node when the
* lookup lands on a shared Folder/Project node (#787, #842) — same
* guard resolve_file_calls/usages/rw already use. */
const cbm_gbuf_node_t *src =
find_source_node(rc->main_gbuf, rc->project_name, rel, thr->enclosing_func_qn);
if (!src) {
continue;
}
const char *edge_type = is_checked_exception(thr->exception_name) ? "THROWS" : "RAISES";
cbm_resolution_t res = cbm_registry_resolve_lang(
rc->registry, thr->exception_name, module_qn, imp_keys, imp_vals, imp_count, lang);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
}
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
if (!tgt || src->id == tgt->id) {
continue;
}
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, edge_type, "{}");
}
}
/* Resolve reads/writes for one file. */
static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
if (!rw->var_name) {
continue;
}
const cbm_gbuf_node_t *src =
find_source_node(rc->main_gbuf, rc->project_name, rel, rw->enclosing_func_qn);
if (!src) {
continue;
}
/* C member read/write: same owner join as resolve_file_usages. Mirrors
* the sequential twin (pass_usages.c). */
if (cbm_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel), rw->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
rw->enclosing_func_qn, rw->var_name);
const char *member_edge = rw->is_write ? "WRITES" : "READS";
const CBMResolvedCall *owner;
bool typed = false;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
typed = true;
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, member_edge, "{}");
}
if (!typed) {
const char *only = cbm_registry_unique_field_qn(rc->registry, rw->var_name);
const cbm_gbuf_node_t *field =
only ? cbm_gbuf_find_by_qn(rc->main_gbuf, only) : NULL;
if (field && field->id != src->id &&
!cbm_suppress_cross_language_ref(lang, field->file_path)) {
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, member_edge, "{}");
}
}
continue;
}
cbm_resolution_t res = cbm_registry_resolve_lang(rc->registry, rw->var_name, module_qn,
imp_keys, imp_vals, imp_count, lang);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
continue;
}
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(
rc->main_gbuf, cbm_registry_value_target(rc->registry, res.qualified_name));
if (!tgt || src->id == tgt->id) {
continue;
}
/* #1928: every resolution here is a bare-name registry guess — never
* let it bind a read/write across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct field;
* a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
continue;
}
const char *etype = rw->is_write ? "WRITES" : "READS";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
}
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* The per-file facts the base-class gate needs beyond the import map. */
typedef struct {
const CBMImportArray *imports;
const char *rel;
CBMLanguage lang;
} pp_file_scope_t;
/* Resolve base_classes → INHERITS edges for one definition. */
static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
const CBMDefinition *def, const cbm_gbuf_node_t *node,
const pp_file_scope_t *fs, const char *mq, const char **ik,
const char **iv, int ic,
const cbm_php_vendor_names_t *php_vendor) {
if (!def->base_classes) {
return;
}
for (int b = 0; def->base_classes[b]; b++) {
/* #1186: `extends Request` with `use Saloon\Http\Request` names the
* vendor class, never a same-named project class. Mirrors
* pass_semantic.c. */
if (cbm_pipeline_php_vendor_bound(php_vendor, def->base_classes[b])) {
continue;
}
/* Same order as the sequential semantic pass: an earlier sibling of
* the class body first (no external-base gate), then the registry. */
const char *bqn = cbm_registry_resolve_python_class_body_base(
rc->registry, rc->main_gbuf, def->qualified_name, def->base_classes[b], fs->lang);
if (!bqn) {
bqn = resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic, fs->lang);
if (!bqn || cbm_python_external_base_contradicts(
fs->lang, fs->imports, def->base_classes[b], bqn, rc->main_gbuf,
rc->project_name, fs->rel)) {
continue;
}
}
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, bqn);
if (bn && node->id != bn->id) {
/* Same Interface-label split as the sequential semantic pass —
* hardcoding INHERITS here demoted every explicit `implements`
* on large (parallel-path) corpora. */
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, bn->id,
cbm_semantic_base_edge_type(bn), "{}");
ws->semantic_resolved++;
}
}
}
/* Resolve decorators → DECORATES edges for one definition. */
static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws,
const CBMDefinition *def, const cbm_gbuf_node_t *node,
const char *mq, const char **ik, const char **iv, int ic,
CBMLanguage lang) {
if (!def->decorators) {
return;
}
for (int dc = 0; def->decorators[dc]; dc++) {
char fn[CBM_SZ_256];
extract_decorator_func(def->decorators[dc], fn, sizeof(fn));
if (fn[0] == '\0') {
continue;
}
cbm_resolution_t res = cbm_registry_resolve_lang(rc->registry, fn, mq, ik, iv, ic, lang);
if ((!res.qualified_name || res.qualified_name[0] == '\0') && !strchr(fn, '.')) {
/* C# attributes are referenced by their short name (`[Log]`) but
* declared with an `Attribute` suffix (`class LogAttribute`). */
char with_suffix[CBM_SZ_256];
int wn = snprintf(with_suffix, sizeof(with_suffix), "%sAttribute", fn);
if (wn > 0 && (size_t)wn < sizeof(with_suffix)) {
res = cbm_registry_resolve_lang(rc->registry, with_suffix, mq, ik, iv, ic, lang);
}
}
const cbm_gbuf_node_t *dn = NULL;
if (res.qualified_name && res.qualified_name[0] != '\0') {
dn = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
}
/* Qualified Rust proc-macro paths must not degrade to the decorated
* function itself through the registry's same-module suffix fallback
* (`#[tokio::main]` on local `main`). Preserve the full external path
* through the synthetic Decorator node instead. */
if (dn && dn->id == node->id && def->decorators[dc][0] == '#' &&
def->decorators[dc][1] == '[' && strstr(fn, "::")) {
dn = NULL;
}
int64_t dn_id = 0;
if (dn) {
dn_id = dn->id;
} else {
/* External/stdlib decorator (Rust `#[derive(Debug)]`, Swift
* `@discardableResult`, Scala `@deprecated`, Python `@cache`,
* Java `@Override`, ...): no local symbol resolves. Materialise a
* synthetic "Decorator" node so the DECORATES relation is recorded.
* The node is created in the per-worker local_edge_buf (shared-ID
* gbuf); the sequential merge dedupes by QN across workers, so all
* uses of the same decorator name collapse to one node project-wide
* and the edge target IDs are remapped consistently. */
char syn_qn[CBM_SZ_512];
snprintf(syn_qn, sizeof(syn_qn), "<decorator:%s>", fn);
dn_id =
cbm_gbuf_upsert_node(ws->local_edge_buf, "Decorator", fn, syn_qn, "", 0, 0, "{}");
}
if (dn_id != 0 && node->id != dn_id) {
/* Decorator SOURCE TEXT can contain quotes and raw newlines
* (e.g. @register.tag("block"), multi-line @override_settings) —
* interpolating it raw produced malformed properties JSON that
* aborts every json_extract consumer (django: 3826 such edges). */
char esc_dec[CBM_SZ_256];
cbm_json_escape(esc_dec, sizeof(esc_dec), def->decorators[dc]);
char dp[CBM_SZ_512];
snprintf(dp, sizeof(dp), "{\"decorator\":\"%s\"}", esc_dec);
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn_id, "DECORATES", dp);
/* Ensure a reference-style edge exists so the decorator appears in queries
* without being misclassified as a real call by downstream passes. */
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn_id, "USAGE", "{}");
ws->semantic_resolved++;
}
}
}
/* Resolve INHERITS + DECORATES + IMPLEMENTS for one file. */
static void resolve_file_semantic(resolve_ctx_t *rc, resolve_worker_state_t *ws,
CBMFileResult *result, const char *rel, CBMLanguage lang,
const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count,
const cbm_php_vendor_names_t *php_vendor) {
const pp_file_scope_t fs = {&result->imports, rel, lang};
for (int d = 0; d < result->defs.count; d++) {
CBMDefinition *def = &result->defs.items[d];
if (!def->qualified_name) {
continue;
}
const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(rc->main_gbuf, def->qualified_name);
if (!node) {
continue;
}
resolve_def_inherits(rc, ws, def, node, &fs, module_qn, imp_keys, imp_vals, imp_count,
php_vendor);
resolve_def_decorators(rc, ws, def, node, module_qn, imp_keys, imp_vals, imp_count, lang);
}
for (int t = 0; t < result->impl_traits.count; t++) {
CBMImplTrait *it = &result->impl_traits.items[t];
if (!it->trait_name || !it->struct_name) {
continue;
}
const char *tqn = resolve_as_class(rc->registry, it->trait_name, module_qn, imp_keys,
imp_vals, imp_count, lang);
const char *sqn = resolve_as_class(rc->registry, it->struct_name, module_qn, imp_keys,
imp_vals, imp_count, lang);
if (!tqn || !sqn) {
continue;
}
const cbm_gbuf_node_t *tn = cbm_gbuf_find_by_qn(rc->main_gbuf, tqn);
const cbm_gbuf_node_t *sn = cbm_gbuf_find_by_qn(rc->main_gbuf, sqn);
if (tn && sn && tn->id != sn->id) {
cbm_gbuf_insert_edge(ws->local_edge_buf, sn->id, tn->id, "IMPLEMENTS", "{}");
ws->semantic_resolved++;
}
}
}
/* F4: get (or lazily build ONCE) the shared all_defs Rust registry. Called by the
* first worker that hits a NULL-filter rust file; later null-files reuse it. Fast
* path is a lock-free atomic load; the build happens under rust_shared_mu into the
* dedicated rust_shared_arena (never a shared pipeline arena from a worker thread).
* Returns NULL if there are no defs (caller falls back to the per-file build). */
static CBMTypeRegistry *pp_rust_shared_registry(resolve_ctx_t *rc) {
CBMTypeRegistry *p = atomic_load_explicit(&rc->rust_shared_reg, memory_order_acquire);
if (p)
return p;
if (!rc->all_defs || rc->def_count <= 0)
return NULL;
cbm_mutex_lock(&rc->rust_shared_mu);
p = atomic_load_explicit(&rc->rust_shared_reg, memory_order_relaxed);
if (!p) {
cbm_arena_init(&rc->rust_shared_arena);
rc->rust_shared_arena_live = true;
p = cbm_rust_build_cross_registry(&rc->rust_shared_arena, rc->all_defs, rc->def_count);
if (p) {
char sb[96];
snprintf(sb, sizeof(sb), "types=%d funcs=%d", p->type_count, p->func_count);
cbm_log_info("cross_lsp.rust_registry", "scale", sb);
}
atomic_store_explicit(&rc->rust_shared_reg, p, memory_order_release);
}
cbm_mutex_unlock(&rc->rust_shared_mu);
return p;
}
/* void*-typed adapter so the shared dispatch helper (pass_lsp_cross.c) can
* borrow the lazily-built shared Rust registry without knowing resolve_ctx_t. */
static CBMTypeRegistry *pp_rust_shared_registry_get(void *ctx) {
return pp_rust_shared_registry((resolve_ctx_t *)ctx);
}
static int pp_call_reference_site_count(const CBMFileResult *result, CBMLanguage lang) {
int count = 0;
for (int i = 0; i < result->usages.count; i++) {
if (cbm_pipeline_usage_semantic_reference_candidate(&result->usages.items[i])) {
count++;
}
}
return count;
}
static int pp_qualified_lsp_site_count(const CBMFileResult *result) {
int count = 0;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if ((resolved->kind == CBM_RESOLVED_INVOCATION ||
resolved->kind == CBM_RESOLVED_CALL_REFERENCE) &&
resolved->caller_qn && resolved->callee_qn &&
resolved->confidence >= CBM_LSP_CONFIDENCE_FLOOR) {
count++;
}
}
return count;
}
/* A per-file semantic walk can discover a faithful source occurrence that the
* syntax extractor cannot represent as a CBMCall yet: Python operators and
* calls hidden in Rust built-in macro token trees are the canonical examples.
* An exact unresolved/low-confidence record is therefore an explicit request
* for the project-registry pass, even when the parser-backed site count is
* zero or already fully qualified. Zero-span generated semantics are excluded:
* without source provenance they cannot safely join a graph carrier. */
static bool pp_has_pending_lsp_site(const CBMFileResult *result) {
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
if (resolved->kind != CBM_RESOLVED_INVOCATION &&
resolved->kind != CBM_RESOLVED_CALL_REFERENCE) {
continue;
}
if (!resolved->caller_qn || !resolved->callee_qn ||
resolved->site_end_byte <= resolved->site_start_byte) {
continue;
}
if (resolved->reason || resolved->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
return true;
}
}
return false;
}
static CBMFileResult *resolve_load_result(resolve_ctx_t *rc, resolve_worker_state_t *ws,
int file_idx) {
if (ws->loaded) {
cbm_free_result(ws->loaded);
ws->loaded = NULL;
}
CBMFileResult *result = rc->result_cache[file_idx];
if (!result && rc->pctx && rc->pctx->spill && cbm_result_spill_has(rc->pctx->spill, file_idx)) {
result = cbm_result_spill_load(rc->pctx->spill, file_idx);
ws->loaded = result;
(void)cbm_pipeline_test_extraction_ok(rc->pctx, rc->files[file_idx].language, result);
if (!result) {
cbm_pipeline_mark_unresolved_capture_failed(rc->pctx->pipeline);
}
}
return result;
}
static void resolve_capture_coverage(const resolve_ctx_t *rc, const char *rel,
const CBMFileResult *result, const CBMCallEvidence *evidence) {
cbm_pipeline_record_unresolved_calls(rc->pctx ? rc->pctx->pipeline : NULL, rel, result,
evidence);
}
/* A C member access binds only through a field-owner row. The per-file walk
* publishes owners for the structs its own file declares; a struct from a
* header is known only to the cross-file pass. A member access that still has
* no owner is therefore a request for that pass, exactly like an unqualified
* call site -- without it a file with no pending call would keep none of its
* edges onto header-declared fields. */
static bool pp_has_unowned_c_member(const CBMFileResult *result, CBMLanguage lang,
const char *rel) {
if (!cbm_c_member_rule_file(lang, rel)) {
return false;
}
cbm_pipeline_lsp_field_index_t field_index = {0};
bool ready = cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
bool unowned = false;
for (int i = 0; i < result->usages.count && !unowned; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind != CBM_USAGE_VALUE || !usage->is_member_access || !usage->ref_name) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners =
cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL,
usage->enclosing_func_qn, usage->ref_name);
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
for (int i = 0; i < result->rw.count && !unowned; i++) {
const CBMReadWrite *rw = &result->rw.items[i];
if (!rw->is_member_access || !rw->var_name) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners =
cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL,
rw->enclosing_func_qn, rw->var_name);
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
for (int i = 0; i < result->calls.count && !unowned; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->callee_name ||
(!strchr(call->callee_name, '.') && !strstr(call->callee_name, "->"))) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, ready ? &field_index : NULL, call->enclosing_func_qn,
cbm_lsp_bare_segment(call->callee_name));
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
cbm_pipeline_lsp_field_index_free(&field_index);
return unowned;
}
static void resolve_worker(int worker_id, void *ctx_ptr) {
resolve_ctx_t *rc = ctx_ptr;
resolve_worker_state_t *ws = &rc->workers[worker_id];
cbm_pxc_set_rust_manifest(rc->rust_manifest);
if (!ws->local_edge_buf) {
ws->local_edge_buf = cbm_gbuf_new_worker(rc->project_name, rc->repo_path, rc->shared_ids);
}
/* Per-worker service-pattern result cache. The same resolved QN
* (e.g. "fmt.Errorf", "context.Context.Done") appears in many
* call edges across many files within a project — caching turns
* cbm_service_pattern_match's 6 × 30 × strstr scan into one hash
* lookup after the first miss for each QN. Scoped to the worker's
* lifetime in the parallel_resolve phase. */
cbm_service_pattern_cache_begin();
cbm_pxc_thread_scratch_begin();
file_node_cache_clear();
while (SKIP_ONE) {
int file_idx =
atomic_fetch_add_explicit(&rc->next_file_idx, SKIP_ONE, memory_order_relaxed);
if (file_idx >= rc->file_count) {
break;
}
cbm_scale_tick(&rc->scale, file_idx);
if (atomic_load_explicit(rc->cancelled, memory_order_relaxed)) {
break;
}
uint64_t _loop_t0 = extract_now_ns();
CBMFileResult *result = resolve_load_result(rc, ws, file_idx);
if (!result) {
atomic_fetch_add_explicit(&rc->time_ns_total_loop, extract_now_ns() - _loop_t0,
memory_order_relaxed);
continue;
}
if (!cbm_pipeline_test_result_ok(rc->pctx, result)) {
continue;
}
atomic_fetch_add_explicit(&rc->total_files_visited, 1, memory_order_relaxed);
CBMLanguage lang = rc->files[file_idx].language;
const char *rel = rc->files[file_idx].rel_path;
CBMCallEvidence *evidence = NULL;
/* Skip cross-LSP for machine-generated files — they're huge (10k-
* 70k lines for k8s protobuf/openapi), have low semantic value for
* graph navigation (boilerplate getters/setters/marshal), and
* dominate the cross-LSP wall time when they have even one
* unresolved call (tree-sitter parse on a 70k-line file is ~1-2s).
* The per-file LSP during extract still indexes their defs/calls
* normally — only the cross-file resolution refinement is skipped. */
bool is_generated = false;
if (rel) {
is_generated =
(strstr(rel, ".pb.go") != NULL) || (strstr(rel, "zz_generated") != NULL) ||
(strstr(rel, "_generated.go") != NULL) || (strstr(rel, ".gen.go") != NULL) ||
(strstr(rel, "/applyconfigurations/") != NULL) ||
(strstr(rel, "_pb2.py") != NULL) || (strstr(rel, "_pb2_grpc.py") != NULL) ||
(strstr(rel, ".pb.cc") != NULL) || (strstr(rel, ".pb.h") != NULL) ||
(strstr(rel, ".pb-c.c") != NULL) || (strstr(rel, ".pb-c.h") != NULL);
}
if (result->has_test_definition_owners &&
(!rc->all_defs || rc->def_count <= 0 || !cbm_pxc_has_cross_lsp(lang) || is_generated ||
result->lsp_skipped)) {
cbm_pipeline_test_owner_error(result);
(void)cbm_pipeline_test_result_ok(rc->pctx, result);
continue;
}
/* Cross-file LSP is a per-file tree-sitter re-parse + AST walk +
* registry lookups — ~50-150ms per file. It can only resolve semantic
* sites present in that AST: invocations plus explicit callable
* references. A file containing only a callable reference still needs
* this pass. For non-JVM languages, skip when per-file LSP already
* produced at least as many qualifying typed resolutions as sites.
* Java/Kotlin per-file LSP can fill the count with same-file sites while
* a mixed-source-root Java↔Kotlin site remains unresolved, so JVM
* callers run whenever either kind of site exists. */
bool jvm_cross_lsp = (lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN);
bool rust_workspace_cross_lsp =
(lang == CBM_LANG_RUST && rc->rust_manifest && rc->rust_manifest->member_count > 0);
int call_reference_sites = pp_call_reference_site_count(result, lang);
int semantic_sites = result->calls.count + call_reference_sites;
int qualified_lsp_sites = pp_qualified_lsp_site_count(result);
bool pending_lsp_site = pp_has_pending_lsp_site(result);
bool unowned_c_member = pp_has_unowned_c_member(result, lang, rel);
bool cross_lsp_eligible =
(rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) &&
(((semantic_sites > 0 || pending_lsp_site) &&
(jvm_cross_lsp || rust_workspace_cross_lsp || pending_lsp_site ||
qualified_lsp_sites < semantic_sites)) ||
unowned_c_member) &&
!is_generated) ||
result->has_test_definition_owners;
/* Skip files with nothing else to resolve and no cross-LSP work. */
if (result->calls.count == 0 && result->usages.count == 0 && result->throws.count == 0 &&
result->rw.count == 0 && result->defs.count == 0 && result->impl_traits.count == 0 &&
!cross_lsp_eligible) {
resolve_capture_coverage(rc, rel, result, NULL);
continue;
}
/* Build import map ONCE (read-only access to main_gbuf). The
* same imp_keys/imp_vals feed both the fused cross-file LSP
* step below AND the resolve_file_* chain — no duplicate build. */
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
uint64_t _imp_t0 = extract_now_ns();
cbm_pxc_build_import_map(rc->main_gbuf, rc->project_name, rel, lang, result, &imp_keys,
&imp_vals, &imp_count);
atomic_fetch_add_explicit(&rc->time_ns_import_map, extract_now_ns() - _imp_t0,
memory_order_relaxed);
cbm_php_vendor_names_t php_vendor;
cbm_pipeline_php_vendor_names_build(rc->main_gbuf, rc->project_name, rel, result,
&php_vendor);
/* Per-file is_import_reachable memoization. Spans all 5 resolve
* sub-passes (calls/usages/throws/rw/semantic) which all flow
* through cbm_registry_resolve. Same callee_name appears in
* many call sites — first eval pays the strstr cost, repeats
* are O(1) hash. Imports are constant within a file so the
* cache is sound; invalidated at file exit. */
cbm_registry_reach_cache_begin(result->calls.count + result->usages.count + 64);
/* Per-file import-map prefix → module-qn hash. resolve_import_map
* was doing O(imports) linear strcmp per call; with this it
* becomes O(1). Keys/values borrowed from imp_keys/imp_vals
* which outlive this scope. */
cbm_registry_import_map_cache_begin(imp_keys, imp_vals, imp_count);
/* THE BIG ONE: per-file cache of cbm_registry_resolve results.
* Same callee_name in multiple call sites resolves identically
* within a file (module_qn is fixed) — first call walks the
* strategy chain, repeats are O(1). On K8s this targets the
* 98.7% hot spot in resolve_file_calls (881 of 893s CPU). */
cbm_registry_resolve_cache_begin(result->calls.count + result->usages.count + 64);
char *module_qn =
cbm_pipeline_fqn_module_dir(rc->project_name, rel, pp_module_is_dir(lang));
/* ── Cross-file LSP (FUSED) ─────────────────────────────
* Runs BEFORE resolve_file_calls so its additions to
* result->resolved_calls are picked up by
* cbm_pipeline_find_lsp_resolution when calls become CALLS
* edges. Prefers source bytes retained in result->arena during
* extract; when the low-RAM retention cap dropped this file
* (result->source==NULL) it FALLS BACK to a bounded per-file
* read from disk, freed immediately after the LSP call. This is
* the correctness guarantee: lowering the retention cap trades
* retained RAM for a bounded re-read, it NEVER drops a cross-file
* edge. Only a genuine read failure (deleted/unreadable/oversized)
* leaves source NULL and is counted as skipped_no_source; defs/calls
* already in the extract are unaffected either way.
*
* Slab reclaim afterward: the LSP re-parses via tree-sitter,
* which allocates through this worker's TLS slab. Reclaiming
* here keeps the slab high-water bounded as the resolve phase
* walks across thousands of files in a single worker thread. */
if (cross_lsp_eligible) {
char *lsp_source_owned = NULL;
const char *lsp_source = result->source;
int lsp_source_len = result->source_len;
if ((!lsp_source || lsp_source_len <= 0) && rc->files[file_idx].path) {
/* Retention cap skipped this file — re-read on demand (bounded
* by read_file's cbm_max_file_bytes cap), freed below. */
lsp_source_owned = read_file(rc->files[file_idx].path, &lsp_source_len, NULL, NULL);
lsp_source = lsp_source_owned;
}
if (lsp_source && lsp_source_len > 0) {
const char *def_module = rc->def_modules ? rc->def_modules[file_idx] : module_qn;
uint64_t lsp_t0 = extract_now_ns();
/* Shared per-file dispatch (pass_lsp_cross.c): module-def
* filter → shared prebuilt registry (overlay pattern) →
* filtered per-file fallback. The SAME helper drives the
* sequential pass — one path, one semantics. Journal the
* file around the resolve so a hang HERE is attributed to
* this file, not to a stale extraction marker. */
cbm_index_mark_start(rel);
bool had_error = result->has_error;
/* The LSP map also binds annotation-only imports (#1277);
* only a file that records one needs its own map, every
* other file passes the CALLS map unchanged. MUST match the
* sequential dispatch in pass_lsp_cross.c. */
const char **lsp_keys = NULL;
const char **lsp_vals = NULL;
int lsp_count = 0;
bool own_lsp_map = cbm_pxc_result_has_type_only_import(result);
if (own_lsp_map) {
cbm_pxc_build_import_map_ex(rc->main_gbuf, rc->project_name, rel, lang, result,
true, &lsp_keys, &lsp_vals, &lsp_count);
}
cbm_pxc_dispatch_file_with_php_bindings(
lang, result, lsp_source, lsp_source_len, rel, def_module, rc->cross_registries,
rc->module_def_index, rc->all_defs, rc->def_count,
own_lsp_map ? lsp_keys : imp_keys, own_lsp_map ? lsp_vals : imp_vals,
own_lsp_map ? lsp_count : imp_count, pp_rust_shared_registry_get, rc,
&php_vendor);
if (own_lsp_map) {
cbm_pxc_free_import_map(lsp_keys, lsp_vals, lsp_count);
}
cbm_index_mark_done(rel);
if (!had_error && result->has_error && result->error_msg &&
(strcmp(result->error_msg, CBM_C_LSP_MEMO_ERROR) == 0 ||
strcmp(result->error_msg, CBM_PY_LSP_MEMO_ERROR) == 0 ||
strcmp(result->error_msg, CBM_C_LSP_DEPTH_ERROR) == 0 ||
strcmp(result->error_msg, CBM_PY_LSP_DEPTH_ERROR) == 0 ||
strcmp(result->error_msg, CBM_PY_LSP_SCOPE_ERROR) == 0)) {
rc->lsp_failures[file_idx] = result->error_msg;
ws->errors++;
}
/* Free the on-demand re-read (no-op when source was retained). */
free_source(lsp_source_owned);
/* Contract: cbm_slab_reclaim() requires the thread parser to be
* destroyed first; otherwise its lexer holds slab pointers
* (lexer.included_ranges) that get freed underneath it, causing
* a heap-use-after-free on the next ts_lexer_goto. The next
* cbm_extract_file on this thread will recreate the parser. */
cbm_destroy_thread_parser();
cbm_slab_reclaim();
uint64_t lsp_elapsed_ns = extract_now_ns() - lsp_t0;
atomic_fetch_add_explicit(&rc->time_ns_cross_lsp, lsp_elapsed_ns,
memory_order_relaxed);
uint64_t lsp_elapsed_ms = lsp_elapsed_ns / PP_USEC_PER_MS;
if (lsp_elapsed_ms > PP_TIMER_THRESH) {
cbm_log_info("parallel.resolve.lsp_cross.slow", "elapsed_ms",
itoa_log((int)lsp_elapsed_ms), "path", rel);
}
atomic_fetch_add_explicit(&rc->lsp_cross_processed, SKIP_ONE, memory_order_relaxed);
if (!cbm_pipeline_test_result_ok(rc->pctx, result)) {
goto resolve_file_cleanup;
}
} else {
/* Source unavailable even after the re-read fallback (file
* deleted / unreadable / oversized) → the cross-file LSP
* refinement no-ops for this file. This is a bounded skip, NOT
* a file failure: defs/calls were already extracted and are
* unaffected. Deliberately NOT recorded as a cbm_file_error —
* doing so would flood skipped[] with false positives (itself a
* false-guard bug). The "cross_lsp" phase string is reserved for
* Track C's real crash-attribution signal; leave it unwired. */
free_source(lsp_source_owned);
if (result->has_test_definition_owners) {
cbm_pipeline_test_owner_error(result);
(void)cbm_pipeline_test_result_ok(rc->pctx, result);
goto resolve_file_cleanup;
}
atomic_fetch_add_explicit(&rc->lsp_cross_skipped_no_source, SKIP_ONE,
memory_order_relaxed);
}
}
/* Per-sub-phase wall-clock so we can attribute the dominant cost. */
uint64_t _ph_t0;
/* Cross-LSP may append synthetic calls; their positions are now final. */
evidence = resolve_evidence_new(rc, result);
/* ── CALLS resolution ──────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_calls(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang,
&php_vendor, evidence);
atomic_fetch_add_explicit(&rc->time_ns_calls, extract_now_ns() - _ph_t0,
memory_order_relaxed);
/* ── USAGE resolution ──────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_usages(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang,
&php_vendor);
atomic_fetch_add_explicit(&rc->time_ns_usages, extract_now_ns() - _ph_t0,
memory_order_relaxed);
/* ── THROWS / RAISES ───────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_throws(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
atomic_fetch_add_explicit(&rc->time_ns_throws, extract_now_ns() - _ph_t0,
memory_order_relaxed);
/* ── READS / WRITES ────────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_rw(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
atomic_fetch_add_explicit(&rc->time_ns_rw, extract_now_ns() - _ph_t0, memory_order_relaxed);
/* ── INHERITS + DECORATES + IMPLEMENTS ──────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_semantic(rc, ws, result, rel, lang, module_qn, imp_keys, imp_vals, imp_count,
&php_vendor);
atomic_fetch_add_explicit(&rc->time_ns_semantic, extract_now_ns() - _ph_t0,
memory_order_relaxed);
/* ── MENTIONS (doc-comment references) ─────────────────── */
if (rc->pctx && rc->pctx->doc_links) {
cbm_doclinks_resolve_file(rc->pctx->doc_links, file_idx, result, rc->main_gbuf,
ws->local_edge_buf);
}
resolve_file_cleanup:
/* Capture the resolved file and side evidence before releasing either,
* including files loaded from the spill store. */
resolve_capture_coverage(rc, rel, result, evidence);
cbm_free(CBM_MEM_CLASS_RESOLVE, evidence);
cbm_registry_reach_cache_end();
cbm_registry_import_map_cache_end();
cbm_registry_resolve_cache_end();
free(module_qn);
cbm_pipeline_php_vendor_names_free(&php_vendor);
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count);
atomic_fetch_add_explicit(&rc->time_ns_total_loop, extract_now_ns() - _loop_t0,
memory_order_relaxed);
}
/* Tear down this worker's thread-local parser + slab state, mirroring
* extract_worker. Without this, resolve-worker pages keep an owner pointer
* into dead TLS and are never retired, so a later cross-thread free can
* never bring their refcount to zero (leak). Retiring them here releases
* each page as its final chunk returns. */
if (ws->loaded) {
cbm_free_result(ws->loaded);
ws->loaded = NULL;
}
cbm_pxc_set_rust_manifest(NULL);
cbm_destroy_thread_parser();
cbm_slab_destroy_thread();
cbm_service_pattern_cache_end();
cbm_pxc_thread_scratch_end();
file_node_cache_clear();
}
int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count,
CBMFileResult **result_cache, _Atomic int64_t *shared_ids,
int worker_count, CBMLSPDef *all_defs, int def_count,
char *const *def_modules, struct CBMModuleDefIndex *module_def_index,
void *cross_registries_v) {
/* See header: typed as void* across the TU boundary; cast back here. */
CBMCrossLspRegistries *cross_registries = (CBMCrossLspRegistries *)cross_registries_v;
if (file_count == 0) {
return 0;
}
/* Preparation or an opt-out cannot erase owners already found by
* extraction, including results currently parked in the spill store. */
if (atomic_load(&ctx->test_declarations_failed) ||
(atomic_load(&ctx->test_definition_owners_seen) && (!all_defs || def_count <= 0))) {
atomic_store(&ctx->test_declarations_failed, 1);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
if (file_count < 0 || (size_t)file_count > SIZE_MAX / sizeof(const char *)) {
return CBM_NOT_FOUND;
}
cbm_log_info("parallel.resolve.start", "files", itoa_log(file_count), "workers",
itoa_log(worker_count));
resolve_worker_state_t *workers = NULL;
if (cbm_aligned_alloc((void **)&workers, CBM_CACHE_LINE,
(size_t)worker_count * sizeof(resolve_worker_state_t)) != 0) {
return CBM_NOT_FOUND;
}
memset(workers, 0, (size_t)worker_count * sizeof(resolve_worker_state_t));
const char **lsp_failures =
cbm_calloc(CBM_MEM_CLASS_OTHER, (size_t)file_count * sizeof(*lsp_failures));
if (!lsp_failures) {
cbm_aligned_free(workers);
return CBM_NOT_FOUND;
}
bool have_rust = false;
for (int i = 0; i < file_count; i++) {
if (files[i].language == CBM_LANG_RUST &&
(result_cache[i] || (ctx->spill && cbm_result_spill_has(ctx->spill, i)))) {
have_rust = true;
break;
}
}
CBMArena rust_manifest_arena;
CBMCargoManifest rust_manifest;
bool rust_manifest_arena_live = false;
const CBMCargoManifest *rust_manifest_ptr = NULL;
if (have_rust) {
cbm_arena_init(&rust_manifest_arena);
rust_manifest_arena_live = true;
if (cbm_pxc_build_rust_manifest(ctx, &rust_manifest_arena, &rust_manifest)) {
rust_manifest_ptr = &rust_manifest;
}
}
resolve_ctx_t rc = {
.pctx = ctx,
.files = files,
.file_count = file_count,
.project_name = ctx->project_name,
.repo_path = ctx->repo_path,
.workers = workers,
.max_workers = worker_count,
.result_cache = result_cache,
.main_gbuf = ctx->gbuf,
.registry = ctx->registry,
.shared_ids = shared_ids,
.cancelled = ctx->cancelled,
.all_defs = all_defs,
.def_count = def_count,
.def_modules = def_modules,
.module_def_index = module_def_index,
.cross_registries = cross_registries,
.rust_manifest = rust_manifest_ptr,
.lsp_failures = lsp_failures,
};
atomic_init(&rc.next_file_idx, 0);
atomic_init(&rc.lsp_cross_processed, 0);
atomic_init(&rc.lsp_cross_skipped_no_source, 0);
/* F4 lazy shared Rust registry: mutex up before workers spawn. */
atomic_init(&rc.rust_shared_reg, NULL);
cbm_mutex_init(&rc.rust_shared_mu);
rc.rust_shared_arena_live = false;
/* Sub-phase: Dispatch resolve workers (per-file call/usage resolution, PARALLEL) */
CBM_PROF_START(t_resolve_dispatch);
cbm_parallel_for_opts_t opts = {.max_workers = worker_count, .force_pthreads = false};
cbm_scale_begin(&rc.scale, "parallel_resolve", (long)file_count);
cbm_parallel_for(worker_count, resolve_worker, &rc, opts);
cbm_scale_end(&rc.scale);
CBM_PROF_END_N("parallel_resolve", "1_dispatch_workers_parallel", t_resolve_dispatch,
file_count);
for (int i = 0; i < file_count; i++) {
if (lsp_failures[i] && ctx->pipeline)
cbm_pipeline_add_file_error(ctx->pipeline, files[i].rel_path, lsp_failures[i],
"lsp_skipped");
}
cbm_free(CBM_MEM_CLASS_OTHER, lsp_failures);
/* Workers joined: the shared Rust registry (if built) is no longer read.
* Free its dedicated arena + the lock (registry was self-contained: it strdup'd
* all QNs, so freeing all_defs afterward is safe). */
if (rc.rust_shared_arena_live) {
cbm_arena_destroy(&rc.rust_shared_arena);
rc.rust_shared_arena_live = false;
}
cbm_mutex_destroy(&rc.rust_shared_mu);
if (rust_manifest_arena_live) {
cbm_arena_destroy(&rust_manifest_arena);
}
/* Sub-phase: Merge all local edge bufs into main gbuf (SEQUENTIAL) */
CBM_PROF_START(t_resolve_merge);
int total_calls = 0;
int total_usages = 0;
int total_semantic = 0;
int total_lsp_overrides = 0;
for (int i = 0; i < worker_count; i++) {
if (workers[i].local_edge_buf) {
cbm_gbuf_merge(ctx->gbuf, workers[i].local_edge_buf);
total_calls += workers[i].calls_resolved;
total_usages += workers[i].usages_resolved;
total_semantic += workers[i].semantic_resolved;
total_lsp_overrides += workers[i].lsp_overrides;
cbm_gbuf_free(workers[i].local_edge_buf);
}
}
CBM_PROF_END_N("parallel_resolve", "2_merge_edge_bufs_seq", t_resolve_merge,
total_calls + total_usages);
cbm_aligned_free(workers);
if (atomic_load(&ctx->test_declarations_failed)) {
cbm_log_error("pipeline.err", "phase", "test_declarations_resolve");
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
/* Go-style implicit interface satisfaction (needs full graph, serial) */
int go_impl = cbm_pipeline_implements_go(ctx);
/* Explicit-language override detection (same serial full-graph tail the
* sequential pipeline runs — the two venues must emit identical graphs). */
total_lsp_overrides += cbm_pipeline_override_explicit(ctx);
if (atomic_load(ctx->cancelled)) {
return CBM_NOT_FOUND;
}
/* Summary metric that replaces the removed `pass.timing pass=lsp_cross`
* log line — surfaces how many files the fused cross-file LSP step
* actually processed vs skipped (e.g. because their source bytes
* were not retained at extract time due to the per-file/total cap). */
cbm_log_info(
"parallel.resolve.lsp_cross_done", "files_processed",
itoa_log(atomic_load_explicit(&rc.lsp_cross_processed, memory_order_relaxed)),
"files_skipped_no_source",
itoa_log(atomic_load_explicit(&rc.lsp_cross_skipped_no_source, memory_order_relaxed)),
"defs_total", itoa_log(def_count));
/* Cross-LSP cost, NORMALISED (#1669). Wall time alone cannot distinguish
* "big repo" from "superlinear pass"; us_per_file can. For a pass that is
* linear in file count this number is roughly CONSTANT across repo sizes.
* When cross-file LSP rebuilds its registry from a corpus-scaled def set,
* per-file cost tracks defs_total instead — measured 35ms/file at 5.8k
* files and 209ms/file at 46k on the same tree, which is the O(n^2) that
* cost a 6x java regression and took an 11-corpus two-binary A/B to find.
* Emitted next to defs_total so one grep on two differently sized repos
* answers it. */
int cross_files = atomic_load_explicit(&rc.lsp_cross_processed, memory_order_relaxed);
uint64_t cross_us = atomic_load_explicit(&rc.time_ns_cross_lsp, memory_order_relaxed) / 1000ULL;
if (cross_files > 0) {
char cf_buf[CBM_SZ_32];
char nf_buf[CBM_SZ_32];
char cu_buf[CBM_SZ_32];
char pk_buf[CBM_SZ_32];
snprintf(cf_buf, sizeof(cf_buf), "%llu",
(unsigned long long)(cross_us / (uint64_t)cross_files));
snprintf(nf_buf, sizeof(nf_buf), "%d", cross_files);
snprintf(cu_buf, sizeof(cu_buf), "%llu", (unsigned long long)(cross_us / 1000ULL));
snprintf(pk_buf, sizeof(pk_buf), "%llu",
(unsigned long long)(def_count > 0
? (cross_us * 1000ULL) /
((uint64_t)cross_files * (uint64_t)def_count)
: 0ULL));
cbm_log_info("parallel.resolve.cross_lsp_cost", "cross_lsp_ms", cu_buf, "files", nf_buf,
"us_per_file", cf_buf, "defs_total", itoa_log(def_count),
"us_per_file_per_kdef", pk_buf);
/* What the per-file registry build actually cost. defs_per_file is the
* lever: if it tracks defs_total rather than the file's own module plus
* imports, the module filter is not containing the work and cross-file
* LSP is O(files x corpus_defs). */
uint64_t reg_defs = 0;
uint64_t reg_files = 0;
uint64_t flt_files = 0;
uint64_t flt_failed = 0;
cbm_pxc_filter_stats(&reg_defs, &reg_files, &flt_files, &flt_failed);
if (reg_files > 0) {
char rd_buf[CBM_SZ_32];
char ff_buf[CBM_SZ_32];
char fp_buf[CBM_SZ_32];
snprintf(rd_buf, sizeof(rd_buf), "%llu", (unsigned long long)(reg_defs / reg_files));
snprintf(ff_buf, sizeof(ff_buf), "%llu", (unsigned long long)flt_failed);
snprintf(fp_buf, sizeof(fp_buf), "%llu", (unsigned long long)flt_files);
cbm_log_info("parallel.resolve.perfile_registry", "defs_per_file", rd_buf, "defs_total",
itoa_log(def_count), "filtered_files", fp_buf, "filter_failed", ff_buf);
}
}
cbm_log_info("parallel.resolve.done", "calls", itoa_log(total_calls), "usages",
itoa_log(total_usages), "semantic", itoa_log(total_semantic + go_impl),
"lsp_overrides", itoa_log(total_lsp_overrides));
/* Per-sub-phase breakdown so we stop guessing about hot paths.
* Numbers are summed across workers (total CPU-ms, not wall-time).
* Split into multiple log lines because itoa_log uses a 4-slot TLS
* ring buffer — more than 4 values per log_info call would alias
* each other (we hit that bug in the first profiling run). */
char loop_buf[32], visits_buf[32];
snprintf(
loop_buf, sizeof(loop_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_total_loop, memory_order_relaxed) /
1000000ULL));
snprintf(visits_buf, sizeof(visits_buf), "%d",
atomic_load_explicit(&rc.total_files_visited, memory_order_relaxed));
cbm_log_info("parallel.resolve.phase_summary", "total_loop_cpu_ms", loop_buf, "files_visited",
visits_buf);
char imp_buf[32], xls_buf[32], cal_buf[32];
snprintf(
imp_buf, sizeof(imp_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_import_map, memory_order_relaxed) /
1000000ULL));
snprintf(
xls_buf, sizeof(xls_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_cross_lsp, memory_order_relaxed) /
1000000ULL));
snprintf(cal_buf, sizeof(cal_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_calls, memory_order_relaxed) /
1000000ULL));
cbm_log_info("parallel.resolve.phase_ms_a", "import_map", imp_buf, "cross_lsp", xls_buf,
"resolve_calls", cal_buf);
char use_buf[32], thr_buf[32], rw_buf[32], sem_buf[32];
snprintf(use_buf, sizeof(use_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_usages, memory_order_relaxed) /
1000000ULL));
snprintf(thr_buf, sizeof(thr_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_throws, memory_order_relaxed) /
1000000ULL));
snprintf(rw_buf, sizeof(rw_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rw, memory_order_relaxed) /
1000000ULL));
snprintf(sem_buf, sizeof(sem_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_semantic, memory_order_relaxed) /
1000000ULL));
cbm_log_info("parallel.resolve.phase_ms_b", "resolve_usages", use_buf, "resolve_throws",
thr_buf, "resolve_rw", rw_buf, "resolve_semantic", sem_buf);
char src_buf[32], lsp_buf[32], rsv_buf[32], hnt_buf[32], tgt_buf[32], emt_buf[32];
snprintf(
src_buf, sizeof(src_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_source, memory_order_relaxed) /
1000000ULL));
snprintf(
lsp_buf, sizeof(lsp_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_lsp_lookup, memory_order_relaxed) /
1000000ULL));
snprintf(
rsv_buf, sizeof(rsv_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_resolve, memory_order_relaxed) /
1000000ULL));
snprintf(hnt_buf, sizeof(hnt_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_hint, memory_order_relaxed) /
1000000ULL));
snprintf(
tgt_buf, sizeof(tgt_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_target, memory_order_relaxed) /
1000000ULL));
snprintf(emt_buf, sizeof(emt_buf), "%llu",
(unsigned long long)(atomic_load_explicit(&rc.time_ns_rc_emit, memory_order_relaxed) /
1000000ULL));
cbm_log_info("parallel.resolve.calls_breakdown", "find_source", src_buf, "lsp_lookup", lsp_buf,
"resolve", rsv_buf);
cbm_log_info("parallel.resolve.calls_breakdown2", "field_hint", hnt_buf, "find_target", tgt_buf,
"emit_edge", emt_buf);
/* Candidate-scan cost (#1669). `per_lookup` is the diagnostic that matters:
* it is a property of the CORPUS, not of the machine, so it is comparable
* across runs and versions. If it grows with repo size, the tail-match scan
* is turning resolve superlinear — which is precisely what took an
* 11-corpus two-binary A/B to establish the first time. `fallback_rows`
* was already counted but, until now, readable only from a test. */
uint64_t tail_lookups = atomic_load_explicit(&g_lsp_tail_lookups, memory_order_relaxed);
uint64_t tail_cands = atomic_load_explicit(&g_lsp_tail_candidates, memory_order_relaxed);
char tl_buf[CBM_SZ_32];
char tc_buf[CBM_SZ_32];
char tp_buf[CBM_SZ_32];
char fb_buf[CBM_SZ_32];
snprintf(tl_buf, sizeof(tl_buf), "%llu", (unsigned long long)tail_lookups);
snprintf(tc_buf, sizeof(tc_buf), "%llu", (unsigned long long)tail_cands);
snprintf(tp_buf, sizeof(tp_buf), "%llu",
(unsigned long long)(tail_lookups ? tail_cands / tail_lookups : 0ULL));
snprintf(fb_buf, sizeof(fb_buf), "%llu",
(unsigned long long)atomic_load_explicit(&g_lsp_linear_fallback_rows,
memory_order_relaxed));
cbm_log_info("parallel.resolve.scan_cost", "tail_lookups", tl_buf, "tail_candidates", tc_buf,
"per_lookup", tp_buf, "fallback_rows", fb_buf);
return 0;
}