Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
465 changes: 465 additions & 0 deletions internal/cbm/cbm.c

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ typedef struct {
* HTTP_CALLS edge to base + path. Tail fields: zero-init stays valid. */
const char *http_client;
const char *http_base_url;
/* Set when this FILE holds two or more definitions with this QN and label
* (`#if`/`#else` twins, a macro redefined per platform, overloads): the
* graph keeps one node per QN, and this lists every one of those
* definitions' line spans, the surviving one included, as a JSON array
* sorted by start line, in graph_buffer.c's variants schema:
* [{"file_path":"a.c","start_line":10,"end_line":14},
* {"file_path":"a.c","start_line":20,"end_line":26}].
* Carried by the definition the graph keeps for the file (the last by
* start line), which makes it the node's `variants` property. NULL on
* every other definition, and in every language outside
* cbm_is_c_preprocessor_lang. */
const char *variants;
/* Callable identity (#2061): offset of the signature suffix inside
* qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix.
* Always 0 until a language enables its callable_identity mode. */
Expand All @@ -288,6 +300,17 @@ typedef struct {

#define CBM_MAX_CALL_ARGS 8

/* The qualified name of a C-preprocessor macro (`#define NAME ...` in C, C++,
* CUDA, Objective-C, GLSL, ISPC) is `<module QN>.<NAME>#macro`. C keeps macros
* apart from ordinary identifiers, so a macro and a function, type, variable or
* enumerator may legally share a name in one file; the fence gives the macro
* an identity of its own, and the plain `<module QN>.<NAME>` always belongs to
* the definition. `name` stays NAME and the label stays "Macro".
*
* Tie rule for anything that resolves a NAME or a plain QN to a node: the
* definition is the target, and the macro only when no definition is visible. */
#define CBM_MACRO_QN_SUFFIX "#macro"

/* Byte offsets are meaningful only within the source buffer that produced
* them. C/C++/CUDA run both raw and preprocessed extraction passes, and those
* buffers can contain unrelated occurrences at the same numeric span. */
Expand Down
643 changes: 588 additions & 55 deletions internal/cbm/extract_defs.c

Large diffs are not rendered by default.

131 changes: 130 additions & 1 deletion internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -988,16 +988,109 @@ static TSNode resolve_qualified_name(TSNode decl) {
return null_node;
}

bool cbm_c_qualifier_is_recovered(TSNode qid) {
if (ts_node_is_null(qid)) {
return false;
}
uint32_t nc = ts_node_child_count(qid);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_child(qid, i);
if (!ts_node_is_named(c) && strcmp(ts_node_type(c), "::") == 0) {
return ts_node_is_missing(c);
}
}
return false;
}

/* A function DEFINITION's parameter list: empty, `(void)`, or every parameter
* named (variadic `...` allowed). A function-like macro invocation that only
* looks like a declarator — `TEST_BEGIN(test_name)`, whose one "parameter" is
* an untyped identifier read as a type — fails this, so its macro name is never
* mistaken for the defined function's name. */
static bool c_params_name_every_parameter(TSNode params) {
enum { MAX_PARAMS_CHECKED = 64 }; /* indexed child access: keep it small */
uint32_t nc = ts_node_named_child_count(params);
if (nc > MAX_PARAMS_CHECKED) {
return false;
}
for (uint32_t i = 0; i < nc; i++) {
TSNode p = ts_node_named_child(params, i);
const char *pk = ts_node_type(p);
if (strcmp(pk, "comment") == 0 || strcmp(pk, "variadic_parameter") == 0) {
continue;
}
if (!ts_node_is_null(ts_node_child_by_field_name(p, TS_FIELD("declarator")))) {
continue;
}
/* `(void)`: the one parameter is an unnamed primitive type. */
TSNode type = ts_node_child_by_field_name(p, TS_FIELD("type"));
if (nc != 1 || ts_node_is_null(type) || strcmp(ts_node_type(type), "primitive_type") != 0) {
return false;
}
}
return true;
}

TSNode cbm_c_recovered_func_name(TSNode func_declarator) {
TSNode null_node = {0};
if (ts_node_is_null(func_declarator) ||
strcmp(ts_node_type(func_declarator), "function_declarator") != 0) {
return null_node;
}
TSNode params = ts_node_child_by_field_name(func_declarator, TS_FIELD("parameters"));
if (ts_node_is_null(params) || !c_params_name_every_parameter(params)) {
return null_node;
}
TSNode err = ts_node_prev_sibling(params);
if (ts_node_is_null(err) || strcmp(ts_node_type(err), "ERROR") != 0) {
return null_node;
}
/* Only the macro-prefix shape: the ERROR holds nothing but bare identifiers
* (the real name, possibly after further attribute macros). Any other token
* means the region is not a declaration this rule can vouch for. */
enum { MAX_PREFIX_TOKENS = 4 };
uint32_t nc = ts_node_child_count(err);
if (nc == 0 || nc > MAX_PREFIX_TOKENS) {
return null_node;
}
TSNode last = null_node;
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_child(err, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "identifier") != 0 && strcmp(ck, "type_identifier") != 0) {
return null_node;
}
last = c;
}
return last;
}

// Resolve function name from C/C++/CUDA/GLSL declarator chain. Shared canonical
// implementation — see the header for the full rationale (#438).
TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) {
TSNode decl = ts_node_child_by_field_name(func_node, TS_FIELD("declarator"));
/* Set once a recovered `RetT::name` qualifier was stepped through: on that
* path the C++ grammar names the function with a type_identifier. */
bool recovered = false;
for (int depth = 0; depth < CBM_DECLARATOR_DEPTH_LIMIT && !ts_node_is_null(decl); depth++) {
const char *dk = ts_node_type(decl);
if (is_c_terminal_name(dk)) {
if (is_c_terminal_name(dk) || (recovered && strcmp(dk, "type_identifier") == 0)) {
return decl;
}
if (strcmp(dk, "function_declarator") == 0) {
TSNode real = cbm_c_recovered_func_name(decl);
if (!ts_node_is_null(real)) {
return real;
}
}
if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) {
if (cbm_c_qualifier_is_recovered(decl)) {
/* `API RetT name(...)`: the "scope" is the return type, the name
* side carries the real declarator chain. */
decl = ts_node_child_by_field_name(decl, TS_FIELD("name"));
recovered = true;
continue;
}
return resolve_qualified_name(decl);
}
TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
Expand All @@ -1013,6 +1106,36 @@ TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) {
return null_node;
}

/* The C-declarator grammars: same set resolve_func_name_c_family routes through
* cbm_resolve_c_declarator_name_node. */
static bool c_declarator_lang(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
lang == CBM_LANG_GLSL || lang == CBM_LANG_HLSL || lang == CBM_LANG_ISPC ||
lang == CBM_LANG_SLANG || lang == CBM_LANG_OBJC;
}

bool cbm_c_reserved_func_name(const char *name) {
/* Statement/operator keywords: error recovery over preprocessor-split code
* reads `else if (a) (b) {` as a definition `else if(...) {...}`. */
static const char *const kw[] = {"if", "else", "for", "while", "do",
"switch", "case", "default", "return", "break",
"continue", "goto", "sizeof", "typedef", NULL};
if (!name) {
return false;
}
for (const char *const *k = kw; *k; k++) {
if (strcmp(name, *k) == 0) {
return true;
}
}
return false;
}

bool cbm_is_c_preprocessor_lang(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
lang == CBM_LANG_GLSL || lang == CBM_LANG_OBJC || lang == CBM_LANG_ISPC;
}

// Convert a resolved function/method name node to its name string. Most nodes
// map directly to their text, but a C++ conversion-operator's `operator_cast`
// node spans the full "operator bool() const" — this grammar folds the parameter
Expand All @@ -1023,6 +1146,12 @@ TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) {
// `if (obj)`) misses.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang) {
char *text = cbm_node_text(a, name_node, source);
/* A C keyword is never a function name: the definition is an error-recovery
* artifact. No name means no def and no call scope (calls inside fall back
* to the enclosing scope), the same as any unnamed definition. */
if (text && c_declarator_lang(lang) && cbm_c_reserved_func_name(text)) {
return NULL;
}
if (text && strcmp(ts_node_type(name_node), "operator_cast") == 0) {
char *paren = strchr(text, '(');
if (paren) {
Expand Down
31 changes: 31 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,37 @@ const char *cbm_enclosing_func_qn_cached(CBMExtractCtx *ctx, TSNode node);
// enclosing-function attribution — drift between private copies caused #438.
TSNode cbm_resolve_c_declarator_name_node(TSNode func_node);

// tree-sitter error recovery around an unknown leading macro: `API RetT name(...)`
// (`XXH_PUBLIC_API XXH64_hash_t XXH64(...)`, `JEMALLOC_ALWAYS_INLINE T *f(...)`)
// carries one identifier more than the declaration grammar admits, so the parser
// recovers in one of two shapes, and neither names the function:
// - C++ inserts a zero-width MISSING "::" and reads `RetT name` as the
// out-of-line name `RetT::name`; with a pointer return, the name side is a
// pointer_type_declarator that wraps the function_declarator;
// - C and C++ keep RetT as the declarator identifier and put the real name in
// an ERROR node directly before the parameter list.
// cbm_c_qualifier_is_recovered: true when `qid` (a qualified_identifier or
// scoped_identifier) is the first shape, i.e. its "::" token is MISSING.
// cbm_c_recovered_func_name: the real name node of the second shape for a
// function_declarator, or a null node when the declarator is not that shape.
// Shared by every C-family declarator walk (defs, call scope, params, C LSP) so
// the def QN and every caller QN agree on the recovered name.
bool cbm_c_qualifier_is_recovered(TSNode qid);
TSNode cbm_c_recovered_func_name(TSNode func_declarator);

// True for a C keyword that error recovery can surface as a function "name"
// (`else if (a) (b) {` split by #ifdef reads as a definition named `if`).
// cbm_func_name_node_text returns NULL for these in the C-declarator grammars,
// so neither a def nor a call scope is minted; the C LSP applies the same test.
bool cbm_c_reserved_func_name(const char *name);

// True for the languages whose `#define`s become Macro nodes: C, C++, CUDA,
// GLSL, Objective-C and ISPC. The one predicate for what follows from the C
// preprocessor: the `#macro` QN namespace (CBM_MACRO_QN_SUFFIX) and the
// `variants` property (CBMDefinition.variants), because #if branches are what
// lets one of these files define a name more than once.
bool cbm_is_c_preprocessor_lang(CBMLanguage lang);

// Convert a resolved function/method name node to its name string, normalizing a
// C++ conversion-operator's `operator_cast` node (which spans the full
// "operator bool() const") down to "operator bool". Shared by the defs and
Expand Down
28 changes: 26 additions & 2 deletions internal/cbm/lsp/c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -5349,22 +5349,44 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) {

// Navigate declarator to find name and parameters
TSNode cur = decl;
// Set after stepping through a recovered `RetT::name` (see
// cbm_c_qualifier_is_recovered): that path names the function with a
// type_identifier, and the "scope" is the return type, not a class.
bool recovered = false;
for (int depth = 0; depth < 10 && !ts_node_is_null(cur); depth++) {
const char *dk = ts_node_type(cur);

if (strcmp(dk, "function_declarator") == 0) {
TSNode fdecl = ts_node_child_by_field_name(cur, "declarator", 10);
params_node = ts_node_child_by_field_name(cur, "parameters", 10);
// `API RetT name(...)`: the real name sits in an ERROR before the
// parameters; the declarator identifier is the return type.
TSNode real = cbm_c_recovered_func_name(cur);
if (!ts_node_is_null(real)) {
func_name = c_node_text(ctx, real);
break;
}
cur = fdecl;
continue;
}
if (strcmp(dk, "pointer_declarator") == 0 || strcmp(dk, "reference_declarator") == 0) {
if (strcmp(dk, "pointer_declarator") == 0 || strcmp(dk, "reference_declarator") == 0 ||
(recovered && strcmp(dk, "pointer_type_declarator") == 0)) {
if (ts_node_named_child_count(cur) > 0)
cur = ts_node_named_child(cur, ts_node_named_child_count(cur) - 1);
else
break;
continue;
}
if (recovered && strcmp(dk, "type_identifier") == 0) {
func_name = c_node_text(ctx, cur);
break;
}
if ((strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) &&
cbm_c_qualifier_is_recovered(cur)) {
cur = ts_node_child_by_field_name(cur, "name", 4);
recovered = true;
continue;
}
if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) {
func_name = c_node_text(ctx, cur);
// Check if this is a method (has Class:: prefix)
Expand Down Expand Up @@ -5404,7 +5426,9 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) {
break;
}

if (!func_name || !func_name[0])
// A keyword "name" is an error-recovery artifact (see cbm_c_reserved_func_name):
// the def extractor mints no node for it, so no caller QN may name it either.
if (!func_name || !func_name[0] || cbm_c_reserved_func_name(func_name))
return;

const char *func_qn = cbm_test_definition_owner_qn(ctx->test_definition_owners, func_node);
Expand Down
1 change: 1 addition & 0 deletions internal/cbm/result_compact.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_str(c, &d->impl_trait);
cr_str(c, &d->http_client);
cr_str(c, &d->http_base_url);
cr_str(c, &d->variants);
}

static void cr_walk_call(cr_ctx_t *c, CBMCall *call) {
Expand Down
31 changes: 27 additions & 4 deletions src/graph_buffer/graph_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,20 @@ static void gb_record_variants(cbm_gbuf_node_t *survivor, const char *a_label, c

/* ── Node operations ─────────────────────────────────────────────── */

/* First key of the canonical same-QN collision order: how strong a claim the
* entity has on a qualified name. A macro is the weakest: when a "Macro" and
* a definition share a QN, the definition, which carries the body, the
* signature and every edge, owns it whichever comes first.
*
* C-preprocessor macros no longer get here: their QN carries its own
* namespace (`...#macro`, CBM_MACRO_QN_SUFFIX) and cannot equal a definition's.
* The case left is Chialisp, whose `defmacro` / `defmac` defs are labelled
* "Macro" under a plain QN: a macro and a `defun` of one name in one file, or
* in a same-stem `.clsp` / `.clib` pair (one module QN). */
static int gb_qn_claim_rank(const char *label) {
return (label && strcmp(label, "Macro") == 0) ? 0 : 1;
}

int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name,
const char *qualified_name, const char *file_path, int start_line,
int end_line, const char *properties_json) {
Expand Down Expand Up @@ -1036,8 +1050,13 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
* of the same entity with new content) replaces the earlier one,
* deterministically, because intra-file arrival order is fixed. A
* full tie is the same entity re-upserted → refresh in place.
* Kind-disambiguated QNs (the real cure) remain a follow-up. */
int c = strcmp(file_path ? file_path : "", existing->file_path ? existing->file_path : "");
* Ahead of all of these, a definition beats a macro
* (gb_qn_claim_rank). Kind-disambiguated QNs (the real cure) remain
* a follow-up. */
int c = gb_qn_claim_rank(existing->label) - gb_qn_claim_rank(label);
if (c == 0) {
c = strcmp(file_path ? file_path : "", existing->file_path ? existing->file_path : "");
}
if (c == 0) {
c = existing->start_line - start_line;
}
Expand Down Expand Up @@ -1649,9 +1668,13 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
* the node set (and every downstream consumer) run to run. Winner =
* smallest file_path, then LARGEST start_line, then largest
* name/label — one total order, commutative, scheduling-free; a full
* tie is the same entity → refresh from src. */
int c = strcmp(sn->file_path ? sn->file_path : "",
* tie is the same entity → refresh from src. A definition beats a
* macro ahead of all of these (gb_qn_claim_rank). */
int c = gb_qn_claim_rank(existing->label) - gb_qn_claim_rank(sn->label);
if (c == 0) {
c = strcmp(sn->file_path ? sn->file_path : "",
existing->file_path ? existing->file_path : "");
}
if (c == 0) {
c = existing->start_line - sn->start_line;
}
Expand Down
28 changes: 28 additions & 0 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -8553,6 +8553,17 @@ static long node_resolution_score(const cbm_node_t *n) {
label_rank = RES_RANK_OTHER;
}
}
/* Tie rule (CBM_MACRO_QN_SUFFIX): a name that is both a definition and a C
* macro -- a typedef or enumerator next to its rename macro -- resolves to
* the definition. The macro scores just under every other definition, and
* still above Module/File; without this a one-line typedef and its macro
* tie on span and the name reads as ambiguous. */
size_t qn_len = n->qualified_name ? strlen(n->qualified_name) : 0;
size_t fence_len = sizeof(CBM_MACRO_QN_SUFFIX) - SKIP_ONE;
if (label_rank == RES_RANK_OTHER && qn_len > fence_len &&
strcmp(n->qualified_name + qn_len - fence_len, CBM_MACRO_QN_SUFFIX) == 0) {
return RES_RANK_OTHER * (long)RES_LABEL_WEIGHT - SKIP_ONE;
}
long span = (long)n->end_line - (long)n->start_line;
if (span < 0) {
span = 0;
Expand Down Expand Up @@ -13097,6 +13108,23 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
result =
snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors, args);
}

/* Tier 3: the C-macro namespace. A macro's QN is `<module>.<NAME>#macro`
* (CBM_MACRO_QN_SUFFIX), out of reach of '%.X'. Tried only when no
* definition answered to the name above (tie rule), for a short name, a
* partial QN, or the macro's plain QN. */
if (!result) {
char macro_qn[CBM_SZ_512];
int macro_len = snprintf(macro_qn, sizeof(macro_qn), "%s" CBM_MACRO_QN_SUFFIX, qn);
if (macro_len > 0 && (size_t)macro_len < sizeof(macro_qn)) {
tier_nodes = NULL;
tier_count = 0;
cbm_store_find_nodes_by_qn_suffix(store, effective_project, macro_qn, &tier_nodes,
&tier_count);
result = snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors,
args);
}
}
free(qn);
free(project);
if (result) {
Expand Down
Loading
Loading