feat(identity): callable identity plumbing, no language enabled (#2061)

Overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) merge
into one graph node today, and that node collects the CALLS of every
overload. The agreed cure is one signature-qualified callable identity for
all overloading languages:

    callable_qn := base_qn [<tparams>] (params) [cvref]

This change lands the plumbing with every language still at
CBM_CALLABLE_ID_NONE, so the graph is byte-identical; each language is
switched on later in its own change (with its index-format bump).

- internal/cbm/callable_sig.{c,h}: the per-language mode side table
  (NONE|TYPED|LABELED_TYPED|LABELED|ARITY; kept out of CBMLangSpec like
  cbm_string_dispatch_suffixes, since ~160 positional rows would otherwise
  all need the member under -Wmissing-field-initializers), the shared
  builder cbm_callable_sig[_mode] for the def and the call-scope QN, and
  the inverse cbm_qn_callable_base_len[_named]. Suffix contract: no '.',
  '::' or '->'; whitespace-normalized; comments/annotations/attributes
  dropped; qualified types keep their last segment; '->' spelled '=>';
  variadic '[]' (Java) / '~'; C++ top-level cv dropped, arrays decay,
  cvref kept; <= 200 bytes, longer ones keep whole leading entries and end
  in '#<fnv1a64 of the full suffix>)'. The builder is iterative (cursor
  walk, bounded declarator loop) and self-checks against the inverse.
- The inverse is anchored to the node's bare name wherever the name is
  known: real QNs already end in ')' without being callables (a Java field
  whose name is its declarator `X = f()`, make `$(VAR)` targets), and only
  the anchor makes the inverse exact on them.
- Leaf splitters strip a suffix first (no-op today): registry by-name key
  (indexed by the bare name for a suffixed callable), cbm_lsp_bare_segment,
  cbm_pipeline_qn_class_method_tail, pxc_last_component, pxc_qn_leaf,
  go_index_visit_node, collect_prod_defs, sg_qn_prefix_len.
- New unused fields: CBMDefinition.qn_sig_off, CBMResolvedCall.callee_sig
  (relocated by result compaction), CBMRegisteredFunc.sig, CBMLSPDef.sig.
  The lsp_surface codec is unchanged (no format bump in this change).
- get_code_snippet and trace_path gain base-QN tiers
  (cbm_store_find_nodes_by_qn_base) that find nothing until suffixes exist.
- store: cbm_store_find_nodes_by_qn_suffix built its LIKE pattern in a
  512-byte buffer (a long suffix was truncated into a pattern that matched
  nothing) and did not escape '_'/'%' (`my_func` also matched `myXfunc`).
  The pattern is now heap-built, escaped, and matched with ESCAPE '\'.

Proof: full sorted node+edge+lsp_surface dumps are byte-identical before
and after on 7 corpora (this repo, elasticsearch libs, Exposed, dotnet
System.Linq, System.Collections, coreclr/gc, a C corpus).

Refs #2061

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-01 00:10:39 +02:00
1 parent 0f52d30c29
commit 11867dfa74
19 files changed
+2085 -91

No files matched your search

+2
View File
@@ -338,6 +338,7 @@ FOUNDATION_SRCS = \
EXTRACTION_SRCS = \
$(CBM_DIR)/cbm.c \
$(CBM_DIR)/extract_defs.c \
$(CBM_DIR)/callable_sig.c \
$(CBM_DIR)/extract_calls.c \
$(CBM_DIR)/extract_imports.c \
$(CBM_DIR)/extract_usages.c \
@@ -633,6 +634,7 @@ TEST_FOUNDATION_SRCS = \
TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
tests/test_callable_sig.c \
tests/test_extraction_inheritance.c \
tests/test_extraction_imports.c \
tests/test_parse_coverage.c \
File diff suppressed because it is too large. Load diff
+83
View File
@@ -0,0 +1,83 @@
/*
* callable_sig.h — signature-qualified callable identity (#2061).
*
* A callable's graph identity is
*
* callable_qn := base_qn [<tparams>] (params) [cvref]
*
* so overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC)
* become distinct nodes instead of merging into one node that collects every
* overload's CALLS edges. The `name` column stays the bare name.
*
* Every language selects a mode (cbm_callable_identity). A language whose
* mode is CBM_CALLABLE_ID_NONE keeps its historical QN byte for byte; the
* suffix is applied only where a mode is enabled, and the SAME builder
* (cbm_callable_sig) serves the definition QN and the call-scope QN so the two
* can never disagree.
*
* Suffix contract (what cbm_qn_callable_base_len inverts):
* - it never contains '.', "::" or "->" — the separators every QN leaf
* splitter uses — so a splitter that first strips the suffix finds the
* same leaf as before;
* - it is whitespace-normalized: one space only between two identifier
* characters ("unsigned long", "const T&"), none elsewhere;
* - type spelling: comments, annotations and attributes are dropped;
* qualified type paths keep their last segment (java.util.List -> List,
* std::vector -> vector); "->" in function types is spelled "=>"; a
* variadic/rest marker is "[]" for Java (the JVM array it is) and "~"
* everywhere else; C++ top-level cv-qualifiers are dropped and array
* parameters decay to pointers, as the language itself does;
* - it is at most CBM_CALLABLE_SIG_MAX bytes. A longer signature keeps the
* leading whole parameter entries that fit and ends in "#<16 hex>)", the
* FNV-1a 64 of the full uncapped suffix, so identity is preserved.
*/
#ifndef CBM_CALLABLE_SIG_H
#define CBM_CALLABLE_SIG_H
#include "cbm.h"
#include "arena.h"
#include "tree_sitter/api.h"
#include <stddef.h>
typedef enum {
CBM_CALLABLE_ID_NONE = 0, /* historical base QN, no suffix */
CBM_CALLABLE_ID_TYPED, /* (int,String) */
CBM_CALLABLE_ID_LABELED_TYPED, /* (_:Data,to:URL) Swift */
CBM_CALLABLE_ID_LABELED, /* (_:style:) ObjC selector */
CBM_CALLABLE_ID_ARITY, /* (2) dynamic langs */
} CBMCallableIdentity;
enum { CBM_CALLABLE_SIG_MAX = 200 };
/* The identity mode a language uses for callable QNs. Every language is
* CBM_CALLABLE_ID_NONE until its enable change lands (with its index-format
* bump). Kept as a side table, like cbm_string_dispatch_suffixes, rather than
* a CBMLangSpec field: the ~160 positional lang_specs rows would all have to
* spell the new member under -Wmissing-field-initializers. */
CBMCallableIdentity cbm_callable_identity(CBMLanguage lang);
/* The identity suffix for the callable at `func_node` in the language's own
* mode, or NULL when that mode is NONE (or the node carries no parameter
* information the mode can use). Arena-owned. */
const char *cbm_callable_sig(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang);
/* Same, with an explicit mode — the builder the enable changes and the golden
* tables exercise before a language is switched on. */
const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source,
CBMLanguage lang, CBMCallableIdentity mode);
/* Length of the base QN, i.e. `qn` without its callable identity suffix; equal
* to strlen(qn) when there is none. Purely syntactic (the inverse of the
* builder's contract above); a bare C++ `operator()` leaf is recognised as a
* name, not a suffix. NULL -> 0. */
size_t cbm_qn_callable_base_len(const char *qn);
/* The inverse anchored to the callable's bare `name` (the node's name
* column): the suffix is stripped only when the base QN is `name` or ends in
* "." / "::" + `name`. Other QN text can end in ')' as well — a Java field
* whose name is its declarator `X = f()`, a make `$(VAR)` target — and only
* the anchor makes the inverse exact on it. Use this form wherever the name
* is known; NULL name -> the unanchored form. */
size_t cbm_qn_callable_base_len_named(const char *qn, const char *name);
#endif /* CBM_CALLABLE_SIG_H */
+8
View File
@@ -246,6 +246,10 @@ typedef struct {
* HTTP_CALLS edge to base + path. Tail fields: zero-init stays valid. */
const char *http_client;
const char *http_base_url;
/* 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. */
uint32_t qn_sig_off;
} CBMDefinition;
/* Argument captured from a call expression */
@@ -431,6 +435,10 @@ typedef struct {
uint32_t site_start_byte; // exact source occurrence; end > start when present
uint32_t site_end_byte; // exclusive byte offset in the source file
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
/* Callable identity (#2061): the resolved overload's signature suffix,
* appended to callee_qn when the edge is written; NULL = none (always,
* until a language enables its callable_identity mode). */
const char *callee_sig;
} CBMResolvedCall;
typedef struct {
+3
View File
@@ -99,6 +99,9 @@ typedef struct {
* a decorator rebinds the function name, so the undecorated definition is
* not automatically an exact callable-value target. */
const char **decorators;
/* Callable identity (#2061): signature suffix of this def's graph QN;
* qualified_name stays the base QN. NULL = none (always, until enabled). */
const char *sig;
} CBMLSPDef;
// Parse source, build registry from defs + stdlib, run LSP.
+3
View File
@@ -40,6 +40,9 @@ typedef struct {
* remain NULL when raw cross-file provenance is ambiguous; the Rust trait
* flag still prevents that method from being mistaken for inherent. */
const char *impl_trait_qn;
/* Callable identity (#2061): this overload's signature suffix; its graph
* QN is qualified_name + sig. NULL = none (always, until enabled). */
const char *sig;
} CBMRegisteredFunc;
// Registered type with fields and method names.
+1
View File
@@ -357,6 +357,7 @@ static void cr_walk(cr_ctx_t *c, CBMFileResult *r) {
cr_str(c, &r->resolved_calls.items[i].callee_qn);
cr_str(c, &r->resolved_calls.items[i].strategy);
cr_str(c, &r->resolved_calls.items[i].reason);
cr_str(c, &r->resolved_calls.items[i].callee_sig);
}
cr_array(c, (void **)&r->string_refs.items, r->string_refs.count, sizeof(CBMStringRef));
for (int i = 0; i < r->string_refs.count && r->string_refs.items; i++) {
+1 -1
View File
@@ -52,7 +52,7 @@ src/git/git_context.c 18
src/main.c 32
src/mcp/compact_out.c 39
src/mcp/index_supervisor.c 8
src/mcp/mcp.c 784
src/mcp/mcp.c 778
src/pipeline/artifact.c 38
src/pipeline/fqn.c 23
src/pipeline/lsp_resolve.h 4
+76 -44
View File
@@ -58,6 +58,7 @@ enum {
#include "cypher/cypher.h"
#include "discover/discover.h"
#include "pipeline/pipeline.h"
#include "callable_sig.h" /* cbm_qn_callable_base_len */
#include "pipeline/pass_cross_repo.h"
#include "git/git_context.h"
#include "cli/cli.h"
@@ -4552,10 +4553,18 @@ static void sg_lines_str(char *out, size_t sz, int start, int end) {
* ablation (tree > flat/DOT for LLM comprehension), Lost-in-Distance
* (related rows adjacent — grouping by module does exactly that). */
/* qn-prefix = qualified_name minus its last '.'-segment. Returns length. */
static size_t sg_qn_prefix_len(const char *qn) {
const char *last = qn ? strrchr(qn, '.') : NULL;
return last ? (size_t)(last - qn) : 0;
/* qn-prefix = qualified_name minus its last '.'-segment. Returns length.
* The segment is found in the base QN: a callable identity suffix (#2061)
* stays on the row's short name, so prefix + "." + name still rebuilds it. */
static size_t sg_qn_prefix_len(const char *qn, const char *name) {
size_t last = 0;
size_t base_len = cbm_qn_callable_base_len_named(qn, name);
for (size_t i = 0; i < base_len; i++) {
if (qn[i] == '.') {
last = i;
}
}
return last;
}
typedef struct {
@@ -4822,7 +4831,7 @@ static void emit_search_results_grouped_tree(cbm_sb_t *sb, const cbm_search_outp
const cbm_search_result_t *sr = &out->results[i];
const char *qn = sr->node.qualified_name ? sr->node.qualified_name : "";
const char *file = sr->node.file_path ? sr->node.file_path : "";
size_t plen = sg_qn_prefix_len(qn);
size_t plen = sg_qn_prefix_len(qn, sr->node.name);
bool same_group = previous_prefix && strlen(previous_prefix) == plen &&
memcmp(previous_prefix, qn, plen) == 0 && previous_file &&
strcmp(previous_file, file) == 0;
@@ -4978,7 +4987,7 @@ static void emit_search_results_tree_json(yyjson_mut_doc *doc, yyjson_mut_val *r
const cbm_search_result_t *sr = &out->results[i];
const char *qn = sr->node.qualified_name ? sr->node.qualified_name : "";
const char *file = sr->node.file_path ? sr->node.file_path : "";
size_t plen = sg_qn_prefix_len(qn);
size_t plen = sg_qn_prefix_len(qn, sr->node.name);
bool same_group = previous_prefix && strlen(previous_prefix) == plen &&
memcmp(previous_prefix, qn, plen) == 0 && previous_file &&
strcmp(previous_file, file) == 0;
@@ -8888,7 +8897,7 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
}
const char *qn =
tr->visited[i].node.qualified_name ? tr->visited[i].node.qualified_name : "";
size_t plen = sg_qn_prefix_len(qn);
size_t plen = sg_qn_prefix_len(qn, tr->visited[i].node.name);
if (!have_group || strlen(cur_group) != plen || memcmp(cur_group, qn, plen) != 0) {
char *next_group = cbm_strndup(qn, plen);
if (!next_group) {
@@ -9012,7 +9021,7 @@ static void bfs_to_tree_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
char *cur_group = NULL;
for (int i = 0; i < ordered_count; i++) {
const char *qn = ordered[i].node.qualified_name ? ordered[i].node.qualified_name : "";
size_t plen = sg_qn_prefix_len(qn);
size_t plen = sg_qn_prefix_len(qn, ordered[i].node.name);
if (!cur_group || strlen(cur_group) != plen || memcmp(cur_group, qn, plen) != 0) {
char *next_group = cbm_strndup(qn, plen);
if (!next_group) {
@@ -9293,6 +9302,13 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
}
}
}
if (node_count == 0) {
/* A bare base QN names every signature-qualified overload (#2061);
* several overloads report ambiguity below like same-named nodes. */
cbm_store_free_nodes(nodes, 0);
nodes = NULL;
cbm_store_find_nodes_by_qn_base(store, project, func_name, false, &nodes, &node_count);
}
if (node_count == 0) {
yyjson_mut_doc *error_doc = yyjson_mut_doc_new(NULL);
@@ -12667,6 +12683,34 @@ static char *handle_get_file_outline(cbm_mcp_server_t *srv, const char *args) {
return result;
}
/* Respond from one lookup tier's candidates (always consumed), or NULL when
* the tier found nothing. One match answers directly; several prefer the real
* definition (a .c body over a .h declaration, a Function over a Module) so
* an unambiguous-by-preference match resolves without a disambiguation round
* trip; only a genuine tie returns suggestions. */
static char *snippet_from_tier(cbm_mcp_server_t *srv, cbm_node_t *nodes, int count,
const char *input, const char *match_method, bool include_neighbors,
const char *args) {
if (count <= 0) {
cbm_store_free_nodes(nodes, count);
return NULL;
}
bool ambiguous = false;
int sel = count == SKIP_ONE ? 0 : pick_resolved_node(nodes, count, &ambiguous);
if (ambiguous) {
char *result = snippet_suggestions(input, nodes, count);
cbm_store_free_nodes(nodes, count);
return result;
}
cbm_node_t node = {0};
copy_node(&nodes[sel], &node);
cbm_store_free_nodes(nodes, count);
char *result =
build_snippet_response(srv, &node, match_method, include_neighbors, NULL, 0, args);
free_node_contents(&node);
return result;
}
static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
char *qn = cbm_mcp_get_string_arg(args, "qualified_name");
char *project = get_project_arg(args);
@@ -12708,50 +12752,38 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
return result;
}
/* Tier 1b: a bare base QN names every signature-qualified overload of it
* (#2061). Finds nothing for QNs without a callable identity suffix. */
cbm_node_t *tier_nodes = NULL;
int tier_count = 0;
cbm_store_find_nodes_by_qn_base(store, effective_project, qn, false, &tier_nodes, &tier_count);
char *result =
snippet_from_tier(srv, tier_nodes, tier_count, qn, "base", include_neighbors, args);
/* Tier 2: Suffix match — handles partial QNs ("main.HandleRequest")
* and short names ("ProcessOrder") via LIKE '%.X'. */
cbm_node_t *suffix_nodes = NULL;
int suffix_count = 0;
cbm_store_find_nodes_by_qn_suffix(store, effective_project, qn, &suffix_nodes, &suffix_count);
if (suffix_count == SKIP_ONE) {
copy_node(&suffix_nodes[0], &node);
cbm_store_free_nodes(suffix_nodes, suffix_count);
char *result =
build_snippet_response(srv, &node, "suffix", include_neighbors, NULL, 0, args);
free_node_contents(&node);
free(qn);
free(project);
return result;
if (!result) {
tier_nodes = NULL;
tier_count = 0;
cbm_store_find_nodes_by_qn_suffix(store, effective_project, qn, &tier_nodes, &tier_count);
result =
snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors, args);
}
if (suffix_count > SKIP_ONE) {
/* Prefer the real definition (a .c body over a .h declaration, a Function
* over a Module) so an unambiguous-by-preference match resolves directly
* instead of forcing a disambiguation round trip; only a genuine tie still
* returns suggestions. */
bool snip_ambiguous = false;
int ssel = pick_resolved_node(suffix_nodes, suffix_count, &snip_ambiguous);
if (!snip_ambiguous) {
copy_node(&suffix_nodes[ssel], &node);
cbm_store_free_nodes(suffix_nodes, suffix_count);
char *result =
build_snippet_response(srv, &node, "suffix", include_neighbors, NULL, 0, args);
free_node_contents(&node);
/* Tier 2b: the partial/short form of Tier 1b ("Session.upload"). */
if (!result) {
tier_nodes = NULL;
tier_count = 0;
cbm_store_find_nodes_by_qn_base(store, effective_project, qn, true, &tier_nodes,
&tier_count);
result =
snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors, args);
}
free(qn);
free(project);
if (result) {
return result;
}
char *result = snippet_suggestions(qn, suffix_nodes, suffix_count);
cbm_store_free_nodes(suffix_nodes, suffix_count);
free(qn);
free(project);
return result;
}
cbm_store_free_nodes(suffix_nodes, suffix_count);
free(qn);
free(project);
/* Nothing found — guide the caller toward search_graph */
return cbm_mcp_text_result(
+14 -2
View File
@@ -22,6 +22,7 @@
#define CBM_PIPELINE_LSP_RESOLVE_H
#include "cbm.h"
#include "callable_sig.h" /* cbm_qn_callable_base_len — leaf splitters skip the suffix */
#include "graph_buffer/graph_buffer.h"
#include "foundation/constants.h"
@@ -64,7 +65,10 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
return name;
}
const char *seg = name;
for (const char *p = name; *p; p++) {
/* Scan the base only: a callable identity suffix (#2061) belongs to the
* leaf and may itself contain ':' (Swift labels) or '>' (generics). */
const char *end = name + cbm_qn_callable_base_len(name);
for (const char *p = name; p < end; p++) {
/* '.' (dotted QN / Java-style member) and ':' (C++ `::`, last colon
* wins) are member/scope separators. '>' is only a separator when it
* closes the `->` arrow (preceded by '-'); a bare '>' closes a template
@@ -86,7 +90,15 @@ static inline const char *cbm_pipeline_qn_class_method_tail(const char *qn) {
if (!qn) {
return NULL;
}
const char *last = strrchr(qn, '.');
/* The last '.' of the BASE QN: a callable identity suffix (#2061) never
* contains '.', so this is the historical strrchr for every QN. */
const char *last = NULL;
size_t base_len = cbm_qn_callable_base_len(qn);
for (size_t i = 0; i < base_len; i++) {
if (qn[i] == '.') {
last = qn + i;
}
}
if (!last || last == qn) {
return NULL;
}
+9 -1
View File
@@ -6,6 +6,7 @@
#include "foundation/compat_fs.h"
#include "foundation/constants.h"
#include "foundation/str_util.h"
#include "callable_sig.h"
#include <stdio.h>
#include <stdlib.h>
@@ -425,7 +426,14 @@ static void collect_prod_defs(cbm_pipeline_ctx_t *ctx, ens_prod_def_t ***defs_ou
char class_qn[CBM_SZ_256];
class_qn[0] = '\0';
if (xd->qualified_name) {
const char *dot = strrchr(xd->qualified_name, '.');
/* Owner = base QN minus its leaf (#2061 suffixes hold no '.'). */
const char *dot = NULL;
size_t base_len = cbm_qn_callable_base_len_named(xd->qualified_name, xd->name);
for (size_t i = 0; i < base_len; i++) {
if (xd->qualified_name[i] == '.') {
dot = xd->qualified_name + i;
}
}
if (dot) {
int len = (int)(dot - xd->qualified_name);
if (len > 0 && len < CBM_SZ_256) {
+14 -3
View File
@@ -19,6 +19,7 @@
#include "result_spill.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/lsp_resolve.h"
#include "callable_sig.h"
#include "lsp/go_lsp.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
@@ -273,12 +274,20 @@ static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
static bool pxc_is_jvm_lang(CBMLanguage lang);
/* Leaf of the BASE QN: a callable identity suffix (#2061) never contains
* '.', so this equals the historical strrchr split for every QN. */
static const char *pxc_last_component(const char *qn) {
if (!qn) {
return NULL;
}
const char *dot = strrchr(qn, '.');
return dot ? dot + 1 : qn;
const char *leaf = qn;
size_t base_len = cbm_qn_callable_base_len(qn);
for (size_t i = 0; i < base_len; i++) {
if (qn[i] == '.') {
leaf = qn + i + 1;
}
}
return leaf;
}
/* Every return is arena-owned: the fallback spelling is a copy, never the
@@ -377,7 +386,9 @@ static const char *pxc_qn_leaf(const char *name) {
return NULL;
}
const char *leaf = name;
for (const char *p = name; *p; p++) {
/* Base only: a callable identity suffix (#2061) may carry ':' labels. */
const char *end = name + cbm_qn_callable_base_len(name);
for (const char *p = name; p < end; p++) {
if (*p == '.' || *p == ':' || *p == '/' || *p == '\\') {
leaf = p + 1;
}
+9 -1
View File
@@ -23,6 +23,7 @@
#include "foundation/compat_fs.h"
#include "foundation/limits.h"
#include "cbm.h"
#include "callable_sig.h"
#include <stdio.h>
#include <stdlib.h>
@@ -356,7 +357,14 @@ static void go_index_add(go_method_index_t *ix, const char *name, int type_index
static void go_index_visit_node(const cbm_gbuf_node_t *node, void *userdata) {
go_method_index_t *ix = userdata;
const char *qn = node->qualified_name;
const char *dot = qn ? strrchr(qn, '.') : NULL;
/* Last '.' of the base QN (a callable identity suffix never holds one). */
const char *dot = NULL;
size_t base_len = cbm_qn_callable_base_len_named(qn, node->name);
for (size_t i = 0; qn && i < base_len; i++) {
if (qn[i] == '.') {
dot = qn + i;
}
}
if (!dot || dot == qn || !dot[SKIP_ONE]) {
return;
}
+14 -4
View File
@@ -30,6 +30,7 @@ enum { REG_MAX_CANDIDATES = 256 };
#include "foundation/hash_table.h"
#include "foundation/dyn_array.h"
#include "foundation/platform.h"
#include "callable_sig.h" /* cbm_qn_callable_base_len_named: overloads share the name key */
#include <math.h>
#include <stdio.h>
@@ -943,15 +944,24 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
* their dotted form would only widen the bucket the scorer walks.
*
* `name` is NULL or empty only for callers that have no symbol name to
* give; those have the derived key and nothing else. */
* give; those have the derived key and nothing else.
*
* A signature-qualified callable (#2061) is indexed by its bare `name` in
* place of the derived key, so every overload shares one bucket; any other
* QN keeps its historical last-segment key. */
const char *derived = simple_name(qualified_name);
index_under_name(r, derived, owned_qn);
const char *primary =
name && cbm_qn_callable_base_len_named(owned_qn, name) < strlen(owned_qn) ? name : derived;
index_under_name(r, primary, owned_qn);
/* '#' is a QN fence, and extract_defs.c's rust_cfg_qualified_name is the
* only thing in the tree that mints one today. A grammar that starts
* minting a '#' opts into this second key by doing so, whatever it means by
* the fence: its symbols become reachable under the passed name as well,
* and they share that name's bucket with everything else filed under it. */
if (name && name[0] && strchr(derived, '#') && strcmp(name, derived) != 0) {
* and they share that name's bucket with everything else filed under it.
* The passed name is compared with the key already written: a
* signature-qualified callable is filed under `name` already, and its
* capped suffix may carry a '#' of its own. */
if (name && name[0] && strchr(derived, '#') && strcmp(name, primary) != 0) {
index_under_name(r, name, owned_qn);
}
}
+201 -35
View File
@@ -78,6 +78,8 @@ enum {
#include "foundation/compat.h"
#include "foundation/log.h"
#include "foundation/compat_regex.h"
#include "callable_sig.h" /* cbm_qn_callable_base_len: base-match tier */
#include "foundation/mem_core.h" /* cbm_alloc: pattern buffers */
#include "foundation/str_util.h"
#define XXH_INLINE_ALL
@@ -4389,47 +4391,22 @@ int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, co
/* ── FindNodesByQNSuffix ───────────────────────────────────────── */
int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix,
cbm_node_t **out, int *count) {
*out = NULL;
*count = 0;
if (!s || !s->db) {
return CBM_STORE_ERR;
}
/* Match QNs ending with ".suffix" or exactly equal to suffix */
char like_pattern[CBM_SZ_512];
snprintf(like_pattern, sizeof(like_pattern), "%%.%s", suffix);
const char *sql_with_project =
"SELECT id, project, label, name, qualified_name, file_path, "
"start_line, end_line, properties FROM nodes "
"WHERE project = ?1 AND (qualified_name LIKE ?2 OR qualified_name = ?3)";
const char *sql_any = "SELECT id, project, label, name, qualified_name, file_path, "
"start_line, end_line, properties FROM nodes "
"WHERE (qualified_name LIKE ?1 OR qualified_name = ?2)";
sqlite3_stmt *stmt = NULL;
int rc =
sqlite3_prepare_v2(s->db, project ? sql_with_project : sql_any, CBM_NOT_FOUND, &stmt, NULL);
if (rc != SQLITE_OK) {
store_set_error_sqlite(s, "qn_suffix prepare");
return CBM_STORE_ERR;
}
if (project) {
bind_text(stmt, SKIP_ONE, project);
bind_text(stmt, ST_COL_2, like_pattern);
bind_text(stmt, ST_COL_3, suffix);
} else {
bind_text(stmt, SKIP_ONE, like_pattern);
bind_text(stmt, ST_COL_2, suffix);
}
/* Row filter on the raw qualified_name + name columns (NULL keeps every row). */
typedef bool (*store_qn_keep_fn)(const char *qn, const char *name, const void *arg);
/* Step `stmt` (columns as scan_node reads them) into a node array, keeping the
* rows `keep` accepts. Finalizes `stmt`. */
static int store_collect_nodes(cbm_store_t *s, sqlite3_stmt *stmt, store_qn_keep_fn keep,
const void *keep_arg, cbm_node_t **out, int *count) {
int cap = ST_INIT_CAP_8;
int n = 0;
cbm_node_t *nodes = malloc(cap * sizeof(cbm_node_t));
int scan_rc8;
while ((scan_rc8 = sqlite3_step(stmt)) == SQLITE_ROW) {
if (keep && !keep((const char *)sqlite3_column_text(stmt, CBM_SZ_4),
(const char *)sqlite3_column_text(stmt, CBM_SZ_3), keep_arg)) {
continue;
}
if (n >= cap) {
cap *= ST_GROWTH;
nodes = safe_realloc(nodes, cap * sizeof(cbm_node_t));
@@ -4452,6 +4429,195 @@ int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const
return CBM_STORE_OK;
}
/* "%." + text + tail with LIKE's wildcards ('%', '_') and the escape
* character escaped, for `LIKE ? ESCAPE '\'`. Heap-owned. The caller's text
* is matched literally at any length: a fixed 512-byte buffer used to
* truncate a long suffix into a pattern that matched nothing, and an
* unescaped '_' in `my_func` also matched `myXfunc`. */
static char *store_like_dot_suffix(const char *text, const char *tail) {
size_t n = strlen(text);
size_t tail_len = strlen(tail);
char *pat = cbm_alloc(CBM_MEM_CLASS_STORE, (2 * n) + tail_len + ST_COL_3); /* "%." + NUL */
if (!pat) {
return NULL;
}
size_t k = 0;
pat[k++] = '%';
pat[k++] = '.';
for (size_t i = 0; i < n; i++) {
if (text[i] == '%' || text[i] == '_' || text[i] == '\\') {
pat[k++] = '\\';
}
pat[k++] = text[i];
}
memcpy(pat + k, tail, tail_len + SKIP_ONE);
return pat;
}
int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix,
cbm_node_t **out, int *count) {
*out = NULL;
*count = 0;
if (!s || !s->db || !suffix) {
return CBM_STORE_ERR;
}
/* Match QNs ending with ".suffix" or exactly equal to suffix */
char *like_pattern = store_like_dot_suffix(suffix, "");
if (!like_pattern) {
return CBM_STORE_ERR;
}
const char *sql_with_project =
"SELECT id, project, label, name, qualified_name, file_path, "
"start_line, end_line, properties FROM nodes "
"WHERE project = ?1 AND (qualified_name LIKE ?2 ESCAPE '\\' OR qualified_name = ?3)";
const char *sql_any = "SELECT id, project, label, name, qualified_name, file_path, "
"start_line, end_line, properties FROM nodes "
"WHERE (qualified_name LIKE ?1 ESCAPE '\\' OR qualified_name = ?2)";
sqlite3_stmt *stmt = NULL;
int rc =
sqlite3_prepare_v2(s->db, project ? sql_with_project : sql_any, CBM_NOT_FOUND, &stmt, NULL);
if (rc != SQLITE_OK) {
cbm_free(CBM_MEM_CLASS_STORE, like_pattern);
store_set_error_sqlite(s, "qn_suffix prepare");
return CBM_STORE_ERR;
}
if (project) {
bind_text(stmt, SKIP_ONE, project);
bind_text(stmt, ST_COL_2, like_pattern);
bind_text(stmt, ST_COL_3, suffix);
} else {
bind_text(stmt, SKIP_ONE, like_pattern);
bind_text(stmt, ST_COL_2, suffix);
}
rc = store_collect_nodes(s, stmt, NULL, NULL, out, count);
cbm_free(CBM_MEM_CLASS_STORE, like_pattern);
return rc;
}
/* ── FindNodesByQNBase ─────────────────────────────────────────── */
typedef struct {
const char *base;
size_t len;
bool suffix;
} store_qn_base_arg_t;
/* ASCII case-insensitive equality: SQLite's LIKE folds exactly ASCII. */
static bool store_ascii_ieq(const char *a, const char *b, size_t n) {
for (size_t i = 0; i < n; i++) {
unsigned char x = (unsigned char)a[i];
unsigned char y = (unsigned char)b[i];
if (x >= 'A' && x <= 'Z') {
x = (unsigned char)(x - 'A' + 'a');
}
if (y >= 'A' && y <= 'Z') {
y = (unsigned char)(y - 'A' + 'a');
}
if (x != y) {
return false;
}
}
return true;
}
/* Keep a row whose QN carries a callable identity suffix (#2061) over `base`:
* its base QN equals `base` (exact) or ends with "." + `base` (suffix mode,
* ASCII case-insensitive like the LIKE suffix tier). An unsuffixed QN never
* qualifies, so this tier finds nothing until a language mints suffixes. */
static bool store_qn_base_keep(const char *qn, const char *name, const void *argp) {
const store_qn_base_arg_t *arg = argp;
if (!qn || !name) {
return false;
}
size_t base_len = cbm_qn_callable_base_len_named(qn, name);
if (qn[base_len] == '\0') {
return false;
}
if (!arg->suffix) {
return base_len == arg->len && memcmp(qn, arg->base, arg->len) == 0;
}
return base_len > arg->len && qn[base_len - arg->len - SKIP_ONE] == '.' &&
store_ascii_ieq(qn + base_len - arg->len, arg->base, arg->len);
}
/* Exact mode reads the qualified_name index range [base "(", base "="): a
* suffix starts with '(' (0x28) or '<' (0x3C). Suffix mode needs the leading
* wildcard, like the suffix tier it follows. Both bounds are heap-owned. */
static bool store_qn_base_bounds(const char *base, bool suffix_match, char **lo, char **hi) {
if (suffix_match) {
*lo = store_like_dot_suffix(base, "(%");
*hi = store_like_dot_suffix(base, "<%");
} else {
size_t n = strlen(base);
*lo = cbm_alloc(CBM_MEM_CLASS_STORE, n + ST_COL_2);
*hi = cbm_alloc(CBM_MEM_CLASS_STORE, n + ST_COL_2);
if (*lo && *hi) {
memcpy(*lo, base, n);
memcpy(*hi, base, n);
(*lo)[n] = '(';
(*hi)[n] = '=';
(*lo)[n + SKIP_ONE] = '\0';
(*hi)[n + SKIP_ONE] = '\0';
}
}
if (*lo && *hi) {
return true;
}
cbm_free(CBM_MEM_CLASS_STORE, *lo);
cbm_free(CBM_MEM_CLASS_STORE, *hi);
return false;
}
int cbm_store_find_nodes_by_qn_base(cbm_store_t *s, const char *project, const char *base,
bool suffix_match, cbm_node_t **out, int *count) {
*out = NULL;
*count = 0;
if (!s || !s->db || !base || !base[0]) {
return CBM_STORE_ERR;
}
static const char *const sql[2][2] = {
/* [suffix_match][has project] */
{"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, "
"properties FROM nodes WHERE qualified_name >= ?1 AND qualified_name < ?2",
"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, "
"properties FROM nodes WHERE project = ?1 AND qualified_name >= ?2 AND "
"qualified_name < ?3"},
{"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, "
"properties FROM nodes WHERE (qualified_name LIKE ?1 ESCAPE '\\' OR "
"qualified_name LIKE ?2 ESCAPE '\\')",
"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, "
"properties FROM nodes WHERE project = ?1 AND (qualified_name LIKE ?2 ESCAPE '\\' OR "
"qualified_name LIKE ?3 ESCAPE '\\')"},
};
char *lo = NULL;
char *hi = NULL;
if (!store_qn_base_bounds(base, suffix_match, &lo, &hi)) {
return CBM_STORE_ERR;
}
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql[suffix_match ? 1 : 0][project ? 1 : 0], CBM_NOT_FOUND, &stmt,
NULL) != SQLITE_OK) {
cbm_free(CBM_MEM_CLASS_STORE, lo);
cbm_free(CBM_MEM_CLASS_STORE, hi);
store_set_error_sqlite(s, "qn_base prepare");
return CBM_STORE_ERR;
}
int col = SKIP_ONE;
if (project) {
bind_text(stmt, col++, project);
}
bind_text(stmt, col++, lo);
bind_text(stmt, col, hi);
store_qn_base_arg_t arg = {base, strlen(base), suffix_match};
int rc = store_collect_nodes(s, stmt, store_qn_base_keep, &arg, out, count);
cbm_free(CBM_MEM_CLASS_STORE, lo);
cbm_free(CBM_MEM_CLASS_STORE, hi);
return rc;
}
/* ── NodeDegree ────────────────────────────────────────────────── */
void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *out_deg) {
+7
View File
@@ -105,6 +105,13 @@ int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, co
int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix,
cbm_node_t **out, int *count);
/* Find callables whose signature-qualified QN (#2061) has `base` as its base
* QN (exact), or a base ending with "." + `base` (suffix_match). QNs without
* a callable identity suffix never match: this is the tier that lets a bare
* QN name every overload. */
int cbm_store_find_nodes_by_qn_base(cbm_store_t *s, const char *project, const char *base,
bool suffix_match, cbm_node_t **out, int *count);
/* Get CALLS degree of a node (inbound and outbound). */
void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *out_deg);
+348
View File
@@ -0,0 +1,348 @@
/*
* test_callable_sig.c — golden tables for the signature-qualified callable
* identity builder and its inverse (#2061, plumbing: no language enabled).
*/
#include "test_framework.h"
#include "callable_sig.h"
#include "lang_specs.h"
#include "foundation/arena.h"
#include "pipeline/pipeline.h"
#include "pipeline/lsp_resolve.h"
#include "tree_sitter/api.h"
#include <string.h>
typedef struct {
CBMLanguage lang;
CBMCallableIdentity mode;
const char *source;
const char *kind; /* node kind of the callable */
int nth; /* 0-based occurrence of `kind` in preorder */
const char *expected;
} sig_case_t;
static bool find_nth(TSNode n, const char *kind, int *left, TSNode *out) {
if (strcmp(ts_node_type(n), kind) == 0) {
if (*left == 0) {
*out = n;
return true;
}
(*left)--;
}
uint32_t count = ts_node_child_count(n);
for (uint32_t i = 0; i < count; i++) {
if (find_nth(ts_node_child(n, i), kind, left, out)) {
return true;
}
}
return false;
}
/* Parse `source`, build the nth `kind` node's suffix in `mode`; the returned
* string is arena-owned (NULL when absent). */
static const char *sig_of(CBMArena *a, CBMLanguage lang, CBMCallableIdentity mode,
const char *source, const char *kind, int nth) {
TSParser *parser = ts_parser_new();
ts_parser_set_language(parser, cbm_ts_language(lang));
TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
TSNode node = {0};
int left = nth;
const char *sig = NULL;
if (tree && find_nth(ts_tree_root_node(tree), kind, &left, &node)) {
sig = cbm_callable_sig_mode(a, node, source, lang, mode);
}
ts_tree_delete(tree);
ts_parser_delete(parser);
return sig;
}
static const sig_case_t k_cases[] = {
/* Java: generics kept, qualifiers and annotations dropped, varargs = []. */
{CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED,
"class A { <T extends Comparable<T>> void work(final java.util.List<String> xs, int[] a, "
"String... rest) {} }",
"method_declaration", 0, "(List<String>,int[],String[])"},
{CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { void f(@Ann Map<K, V> m, int a[]){} }",
"method_declaration", 0, "(Map<K,V>,int[])"},
{CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED,
"class A { void f(\n @Ann Map< K ,\n V > m, /* c */ int a [ ]\n) {} }", "method_declaration",
0, "(Map<K,V>,int[])"},
{CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { A(int x){} void g(){} }",
"constructor_declaration", 0, "(int)"},
{CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { A(int x){} void g(){} }",
"method_declaration", 0, "()"},
/* C#: generic arity, ref/out, params inlined by the grammar, tuple names
* dropped, operators and indexers. */
{CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED,
"class A { public void Work<T, U>(ref int x, out string y, params object[] rest, int? n = 3, "
"List<Dictionary<string,int>> m, (int a, string b) t, this Foo z) {} }",
"method_declaration", 0,
"<T,U>(ref int,out string,object[],int?,List<Dictionary<string,int>>,(int,string),Foo)"},
{CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED,
"class A { public static A operator +(A a, A b) => a; }", "operator_declaration", 0, "(A,A)"},
{CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED, "class A { int this[int i] { get => 0; } }",
"indexer_declaration", 0, "(int)"},
/* C++: template kinds, declarator markers, top-level cv dropped, arrays
* decay, function pointers, C variadic, packs, cvref, operators. */
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED,
"struct S { template <typename T, int N = 3> void work(const std::vector<T> & v, "
"int (*cb)(int), unsigned long x, ...) const && noexcept; };",
"template_declaration", 0,
"<typename,int>(const vector<T>&,int(*)(int),unsigned long,~)const&&"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "struct S { S& operator=(S&&) &; };", "field_declaration",
0, "(S&&)&"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "struct S { bool operator()(int) volatile; };",
"field_declaration", 0, "(int)volatile"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED,
"struct S { template<class... Args> void emplace(Args&&... args); };", "template_declaration",
0, "<typename~>(Args&&~)"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void n::S::arr(int a[10], char *const p, int q) {}",
"function_definition", 0, "(int*,char*,int)"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED,
"void w(\n const int a , // trailing\n char const * /* c */ const p\n);",
"declaration", 0, "(int,const char*)"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void v(void);", "declaration", 0, "()"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void f(int (*(*g)(int x))(double), void h(char *s));",
"declaration", 0, "(int(*(*)(int))(double),void(*)(char*))"},
{CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED,
"template <class T> struct S { void f(int); };\ntemplate <class T> void S<T>::f(int x) {}",
"function_definition", 0, "(int)"},
/* Kotlin: extension receiver, vararg, function types, nullable,
* qualified types, suspend. */
{CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED,
"class A {\n fun <T : Comparable<T>> String.work(vararg xs: Int, f: (Int) -> Unit, "
"m: Map<String, List<Int>>? = null, @Ann q: kotlin.collections.List<*>) {}\n}",
"function_declaration", 0, "(this:String,Int~,(Int)=>Unit,Map<String,List<Int>>?,List<*>)"},
{CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED, "fun top(x: Int) = x", "function_declaration", 0,
"(Int)"},
{CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED, "class A { constructor(x: Int) {} }",
"secondary_constructor", 0, "(Int)"},
/* Swift: labels + types (the #2061 Alamofire shape), variadic, inout,
* attributes dropped, qualified types, init/subscript/operators. */
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED,
"class Session {\n func upload<T: Encodable>(_ data: Data, to url: URLConvertible, method: "
"HTTPMethod = .post, headers: [String: String]? = nil, handler: @escaping (Int) -> Void, "
"xs: Int..., io: inout Swift.Int, t: (a: Int, String)) -> UploadRequest { }\n}",
"function_declaration", 0,
"(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void,"
"xs:Int~,io:inout Int,t:(a:Int,String))"},
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED,
"class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}",
"function_declaration", 0, "(_:Data,with:URLRequest)"},
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class C { init(frame: CGRect) {} }",
"init_declaration", 0, "(frame:CGRect)"},
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED,
"class C { subscript(index i: Int) -> Int { return 0 } }", "subscript_declaration", 0,
"(index:Int)"},
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED,
"class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"},
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0,
"()"},
/* Scala: every clause flattened, by-name and repeated parameters. */
{CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED,
"class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: "
"Ordering[T]): Unit = {}\n}",
"function_definition", 0, "(List[T],=>Int,String*,Ordering[T])"},
{CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED,
"class A {\n def f(g: (Int, String) => scala.collection.immutable.Map[String, Int]): Unit\n}",
"function_declaration", 0, "((Int,String)=>Map[String,Int])"},
/* Objective-C: the selector's keyword labels. */
{CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED,
"@implementation Foo\n- (instancetype)initWithFrame:(CGRect)frame style:(NSInteger)style "
"{ return self; }\n@end",
"method_definition", 0, "(_:style:)"},
{CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED, "@implementation Foo\n- (void)run { }\n@end",
"method_definition", 0, "()"},
{CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED,
"@implementation Foo\n- (void)setX:(int)x :(int)y {}\n@end", "method_definition", 0, "(_:_:)"},
{CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED,
"@implementation Foo\n+ (id)make:(int)a, ... { return nil; }\n@end", "method_definition", 0,
"(_:~)"},
/* ARITY (dynamic tier-2 languages), shown on a Swift declaration. */
{CBM_LANG_SWIFT, CBM_CALLABLE_ID_ARITY, "class C { func upload(_ data: Data, to url: URL) {} }",
"function_declaration", 0, "(2)"},
};
TEST(callable_sig_golden_table) {
for (size_t i = 0; i < sizeof(k_cases) / sizeof(k_cases[0]); i++) {
const sig_case_t *tc = &k_cases[i];
CBMArena a;
cbm_arena_init(&a);
const char *sig = sig_of(&a, tc->lang, tc->mode, tc->source, tc->kind, tc->nth);
if (!sig || strcmp(sig, tc->expected) != 0) {
printf(" case %zu: got %s, want %s\n", i, sig ? sig : "(null)", tc->expected);
}
ASSERT_NOT_NULL(sig);
ASSERT_STR_EQ(sig, tc->expected);
ASSERT_TRUE(strlen(sig) <= CBM_CALLABLE_SIG_MAX);
/* Contract: no separator a leaf splitter uses, and the inverse
* recovers the base from a real-shaped QN. */
ASSERT_NULL(strchr(sig, '.'));
ASSERT_NULL(strstr(sig, "::"));
ASSERT_NULL(strstr(sig, "->"));
char *qn = cbm_arena_sprintf(&a, "proj.pkg.Cls.name%s", sig);
ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("proj.pkg.Cls.name"));
cbm_arena_destroy(&a);
}
PASS();
}
/* Long signatures cap at CBM_CALLABLE_SIG_MAX with a hash of the FULL
* suffix: deterministic, still distinct, still invertible. */
TEST(callable_sig_cap_keeps_identity) {
char src_a[4096];
char src_b[4096];
size_t n = (size_t)snprintf(src_a, sizeof(src_a), "class A { void f(");
for (int i = 0; i < 30; i++) {
n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sVeryLongParameterTypeName%d p%d",
i ? ", " : "", i, i);
}
memcpy(src_b, src_a, n);
snprintf(src_a + n, sizeof(src_a) - n, ", int last) {} }");
snprintf(src_b + n, sizeof(src_b) - n, ", long last) {} }");
CBMArena a;
cbm_arena_init(&a);
const char *sa =
sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_a, "method_declaration", 0);
const char *sa2 =
sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_a, "method_declaration", 0);
const char *sb =
sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_b, "method_declaration", 0);
ASSERT_NOT_NULL(sa);
ASSERT_NOT_NULL(sb);
ASSERT_TRUE(strlen(sa) <= CBM_CALLABLE_SIG_MAX);
ASSERT_TRUE(strlen(sb) <= CBM_CALLABLE_SIG_MAX);
ASSERT_STR_EQ(sa, sa2);
ASSERT_STR_NEQ(sa, sb); /* they differ only past the cap: the hash tells */
ASSERT_TRUE(strncmp(sa, "(VeryLongParameterTypeName0,", 28) == 0);
const char *hash = strchr(sa, '#');
ASSERT_NOT_NULL(hash);
ASSERT_EQ(strlen(hash), (size_t)18); /* "#" + 16 hex + ")" */
char *qn = cbm_arena_sprintf(&a, "p.A.f%s", sa);
ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f"));
cbm_arena_destroy(&a);
PASS();
}
/* PR1 is plumbing: no language mints a suffix yet. */
TEST(callable_sig_every_language_is_none) {
CBMArena a;
cbm_arena_init(&a);
const char *src = "class A { void f(int x) {} }";
for (int lang = 0; lang < CBM_LANG_COUNT; lang++) {
ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), CBM_CALLABLE_ID_NONE);
}
TSParser *parser = ts_parser_new();
ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_JAVA));
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src));
TSNode m = {0};
int left = 0;
ASSERT_TRUE(find_nth(ts_tree_root_node(tree), "method_declaration", &left, &m));
ASSERT_NULL(cbm_callable_sig(&a, m, src, CBM_LANG_JAVA));
ts_tree_delete(tree);
ts_parser_delete(parser);
cbm_arena_destroy(&a);
PASS();
}
/* The inverse is the identity on every historical QN shape. */
TEST(callable_sig_base_len_unsuffixed_is_full) {
static const char *const qns[] = {
"proj.pkg.Cls.method",
"proj.S.operator()",
"proj.S.operator<",
"proj.S.operator<=>",
"proj.S.operator->",
"proj.S.operator>>",
"proj.S.operator&&",
"proj.app/(auth)",
"proj.Makefile.$(OBJ)",
"proj.f.(anon)",
"",
"proj.x.list<int>",
"proj.x.make_const",
"a::b::c",
"proj.~Dtor",
"(x)",
"proj.Makefile.foo$(EXT)",
"proj.lib/(group)/page",
"proj.C.+",
"proj.A.unary_!",
};
for (size_t i = 0; i < sizeof(qns) / sizeof(qns[0]); i++) {
ASSERT_EQ(cbm_qn_callable_base_len(qns[i]), strlen(qns[i]));
}
ASSERT_EQ(cbm_qn_callable_base_len(NULL), (size_t)0);
PASS();
}
TEST(callable_sig_base_len_suffixed) {
static const struct {
const char *qn;
const char *base;
} rows[] = {
{"p.S.work(int,String)", "p.S.work"},
{"p.S.work()", "p.S.work"},
{"p.S.operator()()", "p.S.operator()"},
{"p.S.operator()(int)const", "p.S.operator()"},
{"p.S.operator<<typename>(T)", "p.S.operator<"},
{"p.S.operator<=>(S)", "p.S.operator<=>"},
{"p.S.operator->()", "p.S.operator->"},
{"p.S.f<typename,int>(const vector<T>&,int(*)(int))const&&", "p.S.f"},
{"p.C.Work<T,U>(ref int)", "p.C.Work"},
{"p.Session.upload(_:Data,to:URL)", "p.Session.upload"},
{"p.K.work(this:String,(Int)=>Unit)", "p.K.work"},
{"p.O.initWithFrame(_:style:)", "p.O.initWithFrame"},
{"p.C.+(l:C,r:C)", "p.C.+"},
{"p.A.unary_!()", "p.A.unary_!"},
{"p.S.operator/(S)", "p.S.operator/"},
{"p.S.~S()", "p.S.~S"},
{"p.J.f(VeryLong0,#0123456789abcdef)", "p.J.f"},
};
for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
ASSERT_EQ(cbm_qn_callable_base_len(rows[i].qn), strlen(rows[i].base));
}
PASS();
}
/* The leaf splitters look only at the base: Swift labels carry ':' and a
* suffix may carry '>' — neither is a separator. */
TEST(callable_sig_leaf_splitters_skip_suffix) {
const char *swift = "p.Session.upload(_:Data,to:URL)";
ASSERT_STR_EQ(cbm_lsp_bare_segment(swift), "upload(_:Data,to:URL)");
ASSERT_STR_EQ(cbm_pipeline_qn_class_method_tail(swift), "Session.upload(_:Data,to:URL)");
/* Unsuffixed spellings are unchanged. */
ASSERT_STR_EQ(cbm_lsp_bare_segment("Math::square"), "square");
ASSERT_STR_EQ(cbm_lsp_bare_segment("p->run"), "run");
ASSERT_STR_EQ(cbm_lsp_bare_segment("identity<int>"), "identity<int>");
ASSERT_STR_EQ(cbm_pipeline_qn_class_method_tail("p.q.Cls.m"), "Cls.m");
PASS();
}
/* The registry's by-name bucket is the base leaf, so every overload of a
* signature-qualified callable is found by its bare name; an unsuffixed QN
* keeps its historical key (last '.' or "::" segment). */
TEST(callable_sig_registry_by_name_uses_base_leaf) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method");
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method");
cbm_registry_add(r, "square", "p.Math::square", "Function");
const char **out = NULL;
int count = 0;
cbm_registry_find_by_name(r, "upload", &out, &count);
ASSERT_EQ(count, 2);
count = 0;
cbm_registry_find_by_name(r, "square", &out, &count);
ASSERT_EQ(count, 1);
cbm_registry_free(r);
PASS();
}
SUITE(callable_sig) {
RUN_TEST(callable_sig_golden_table);
RUN_TEST(callable_sig_cap_keeps_identity);
RUN_TEST(callable_sig_every_language_is_none);
RUN_TEST(callable_sig_base_len_unsuffixed_is_full);
RUN_TEST(callable_sig_base_len_suffixed);
RUN_TEST(callable_sig_leaf_splitters_skip_suffix);
RUN_TEST(callable_sig_registry_by_name_uses_base_leaf);
}
+2
View File
@@ -892,6 +892,7 @@ extern void suite_subprocess(void);
extern void suite_private_file_lock(void);
extern void suite_lock_registry(void);
extern void suite_extraction(void);
extern void suite_callable_sig(void);
extern void suite_extraction_inheritance(void);
extern void suite_extraction_imports(void);
extern void suite_parse_coverage(void);
@@ -1207,6 +1208,7 @@ int main(int argc, char **argv) {
/* Existing C code regression tests */
RUN_SELECTED_SUITE(ac);
RUN_SELECTED_SUITE(extraction);
RUN_SELECTED_SUITE(callable_sig);
RUN_SELECTED_SUITE(extraction_inheritance);
RUN_SELECTED_SUITE(extraction_imports);
RUN_SELECTED_SUITE(parse_coverage);
+98
View File
@@ -193,6 +193,101 @@ TEST(store_project_delete) {
PASS();
}
/* ── QN suffix / callable-base lookups (#2061) ─────────────────── */
static void qn_lookup_node(cbm_store_t *s, const char *name, const char *qn) {
cbm_node_t n = {.project = "test",
.label = "Method",
.name = name,
.qualified_name = qn,
.file_path = "a.java",
.start_line = 1,
.end_line = 2};
cbm_store_upsert_node(s, &n);
}
/* The suffix tier matches its text literally: '_' is not a LIKE wildcard,
* so `my_func` must not also find `myXfunc`. */
TEST(store_qn_suffix_is_literal) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
qn_lookup_node(s, "my_func", "test.a.my_func");
qn_lookup_node(s, "myXfunc", "test.a.myXfunc");
cbm_node_t *nodes = NULL;
int count = 0;
ASSERT_EQ(cbm_store_find_nodes_by_qn_suffix(s, "test", "my_func", &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 1);
ASSERT_STR_EQ(nodes[0].qualified_name, "test.a.my_func");
cbm_store_free_nodes(nodes, count);
cbm_store_close(s);
PASS();
}
/* A suffix longer than the old 512-byte pattern buffer is not truncated into
* a pattern that matches nothing. */
TEST(store_qn_suffix_is_unbounded) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
char leaf[601];
memset(leaf, 'a', sizeof(leaf) - 1);
leaf[sizeof(leaf) - 1] = '\0';
char long_qn[640];
char long_suffix[640];
snprintf(long_qn, sizeof(long_qn), "test.m.%s", leaf);
snprintf(long_suffix, sizeof(long_suffix), "m.%s", leaf);
qn_lookup_node(s, leaf, long_qn);
cbm_node_t *nodes = NULL;
int count = 0;
ASSERT_EQ(cbm_store_find_nodes_by_qn_suffix(s, "test", long_suffix, &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 1);
ASSERT_STR_EQ(nodes[0].qualified_name, long_qn);
cbm_store_free_nodes(nodes, count);
cbm_store_close(s);
PASS();
}
/* A bare base QN (exact, or as a dotted suffix) finds every signature-
* qualified overload of it, and never an unsuffixed QN — so the tier is inert
* until a language mints suffixes. */
TEST(store_qn_base_finds_overloads_only) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
qn_lookup_node(s, "work", "test.S.work(int)");
qn_lookup_node(s, "work", "test.S.work(String)");
qn_lookup_node(s, "work", "test.S.work");
qn_lookup_node(s, "workx", "test.S.workx(int)");
qn_lookup_node(s, "operator()", "test.S.operator()");
qn_lookup_node(s, "Work", "test.C.Work<T>(T)");
cbm_node_t *nodes = NULL;
int count = 0;
ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.S.work", false, &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 2);
cbm_store_free_nodes(nodes, count);
ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "S.work", true, &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 2);
cbm_store_free_nodes(nodes, count);
ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.C.Work", false, &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 1);
cbm_store_free_nodes(nodes, count);
/* `operator()` is a bare C++ leaf, not "operator" + "()". */
ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.S.operator", false, &nodes, &count),
CBM_STORE_OK);
ASSERT_EQ(count, 0);
cbm_store_free_nodes(nodes, count);
cbm_store_close(s);
PASS();
}
/* ── Node CRUD ──────────────────────────────────────────────────── */
TEST(store_node_crud) {
@@ -2393,6 +2488,9 @@ TEST(store_coverage_replace_rolls_back_when_shadow_rebuild_fails) {
}
SUITE(store_nodes) {
RUN_TEST(store_qn_suffix_is_literal);
RUN_TEST(store_qn_suffix_is_unbounded);
RUN_TEST(store_qn_base_finds_overloads_only);
RUN_TEST(store_coverage_roundtrip_prune_shadow);
RUN_TEST(store_coverage_targeted_path_and_scope_lookup);
RUN_TEST(store_coverage_meta_zero_row_truncation_and_delete);