mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-15 05:00:18 +08:00
Merge pull request #2578 from DeusData/fix/suffix-tiebreak-ref-stubs
fix(registry): break suffix_match ties by kind, C# stub, then QN order, not depth
This commit is contained in:
+85
-35
@@ -223,6 +223,12 @@ static const uint8_t *qn_test_flags(const qn_array_t *arr) {
|
||||
return arr->is_test && arr->is_test_cap >= arr->count ? arr->is_test : NULL;
|
||||
}
|
||||
|
||||
/* The cached candidate languages, under the same coverage rule: both side
|
||||
* arrays grow together and share is_test_cap. */
|
||||
static const uint8_t *qn_lang_flags(const qn_array_t *arr) {
|
||||
return arr->lang && arr->is_test_cap >= arr->count ? arr->lang : NULL;
|
||||
}
|
||||
|
||||
/* Score a candidate for tiebreaking. Higher = better.
|
||||
* Layer 1: Non-test code preferred over test code (+1000)
|
||||
* Layer 2: Namespace proximity via common prefix length (+plen)
|
||||
@@ -237,16 +243,37 @@ static int candidate_score(const char *candidate_qn, const char *module_qn, int
|
||||
return score;
|
||||
}
|
||||
|
||||
/* Number of '.'-separated segments in a QN: how deeply the definition is
|
||||
* nested (file depth + enclosing types). */
|
||||
static int qn_depth(const char *qn) {
|
||||
int depth = 1;
|
||||
for (const char *p = qn; *p; p++) {
|
||||
if (*p == '.') {
|
||||
depth++;
|
||||
}
|
||||
/* How plainly a candidate is something a call can target, lower = better: a
|
||||
* function or method; then any other definition (a class for a constructor
|
||||
* call, a macro, a type); then a candidate whose label is unknown; last a
|
||||
* variable or field, which a call reaches only when it holds a function. */
|
||||
enum {
|
||||
REG_RANK_CALLABLE = 0,
|
||||
REG_RANK_OTHER_DEF = 1,
|
||||
REG_RANK_UNKNOWN = 2,
|
||||
REG_RANK_DATA = 3,
|
||||
};
|
||||
|
||||
static int callable_rank(const cbm_registry_t *r, const char *qn) {
|
||||
const char *label = r ? cbm_registry_label_of(r, qn) : NULL;
|
||||
if (!label) {
|
||||
return REG_RANK_UNKNOWN;
|
||||
}
|
||||
return depth;
|
||||
if (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0) {
|
||||
return REG_RANK_CALLABLE;
|
||||
}
|
||||
if (strcmp(label, "Variable") == 0 || strcmp(label, "Field") == 0) {
|
||||
return REG_RANK_DATA;
|
||||
}
|
||||
return REG_RANK_OTHER_DEF;
|
||||
}
|
||||
|
||||
/* A .NET reference assembly (`src/libraries/<Asm>/ref/<Asm>.cs`) declares an
|
||||
* assembly's public surface with `throw null` bodies in one shallow file; a
|
||||
* call that could equally mean the implementation means the implementation.
|
||||
* C# only: elsewhere a `ref` directory is an ordinary name. */
|
||||
static bool csharp_reference_stub(const char *qn, uint8_t lang) {
|
||||
return lang == CBM_LANG_CSHARP && strstr(qn, ".ref.") != NULL;
|
||||
}
|
||||
|
||||
/* Total order among candidates that tie on candidate_score. The registry's
|
||||
@@ -255,33 +282,49 @@ static int qn_depth(const char *qn) {
|
||||
* that order (kernel: 632 CALLS edges differed between two indexes of the
|
||||
* same tree, `dev_name` flipping between twenty same-named struct fields
|
||||
* and unresolved, `sg_set_buf` between include/linux and tools/virtio).
|
||||
* The rule: the least nested definition wins (a top-level function over a
|
||||
* same-named member two types deep, include/linux over tools/virtio/linux),
|
||||
* then the lexicographically smaller QN — a pure function of the candidate
|
||||
* set, never of the order it was built in (O9). */
|
||||
static bool candidate_outranks_on_tie(const char *candidate, const char *best) {
|
||||
int cd = qn_depth(candidate);
|
||||
int bd = qn_depth(best);
|
||||
if (cd != bd) {
|
||||
return cd < bd;
|
||||
* The rule, a pure function of the candidate set (O9):
|
||||
* 1. the more plainly callable candidate (`dev_name` the function over the
|
||||
* struct fields; Kotlin `where(...)` the method over the property);
|
||||
* 2. for C#, an implementation over a reference-assembly stub;
|
||||
* 3. the lexicographically smaller QN (include/linux over tools/virtio).
|
||||
* Nesting depth is deliberately not a criterion: "least nested wins" sent
|
||||
* django's `Model.objects.filter` to the template `Library.filter`, one
|
||||
* segment shallower than `QuerySet.filter` (2026-10-10 audit). */
|
||||
static bool candidate_outranks_on_tie(const cbm_registry_t *r, const char *candidate,
|
||||
uint8_t candidate_lang, const char *best, uint8_t best_lang) {
|
||||
int ck = callable_rank(r, candidate);
|
||||
int bk = callable_rank(r, best);
|
||||
if (ck != bk) {
|
||||
return ck < bk;
|
||||
}
|
||||
bool cs = csharp_reference_stub(candidate, candidate_lang);
|
||||
bool bs = csharp_reference_stub(best, best_lang);
|
||||
if (cs != bs) {
|
||||
return !cs;
|
||||
}
|
||||
return strcmp(candidate, best) < 0;
|
||||
}
|
||||
|
||||
/* Pick candidate with highest composite score (test-deprioritization + namespace
|
||||
* proximity). `is_test_flags` is the candidates' cached test verdicts, or NULL
|
||||
* when the caller has none (a filtered subset carries its own copy). */
|
||||
static const char *best_by_import_distance(const char **candidates, const uint8_t *is_test_flags,
|
||||
* proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` and
|
||||
* `lang_flags` are the candidates' cached test verdicts and languages, or NULL
|
||||
* when the caller has none (a filtered subset carries its own copies). */
|
||||
static const char *best_by_import_distance(const cbm_registry_t *r, const char **candidates,
|
||||
const uint8_t *is_test_flags, const uint8_t *lang_flags,
|
||||
int count, const char *module_qn) {
|
||||
const char *best = NULL;
|
||||
uint8_t best_lang = (uint8_t)CBM_LANG_COUNT;
|
||||
int best_score = CBM_NOT_FOUND;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int score =
|
||||
candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1);
|
||||
uint8_t lang = lang_flags ? lang_flags[i] : (uint8_t)CBM_LANG_COUNT;
|
||||
if (score > best_score ||
|
||||
(score == best_score && best && candidate_outranks_on_tie(candidates[i], best))) {
|
||||
(score == best_score && best &&
|
||||
candidate_outranks_on_tie(r, candidates[i], lang, best, best_lang))) {
|
||||
best_score = score;
|
||||
best = candidates[i];
|
||||
best_lang = lang;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
@@ -1409,17 +1452,23 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char
|
||||
}
|
||||
|
||||
/* Strategy 4: multiple candidates with import filtering. */
|
||||
static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const char *module_qn,
|
||||
const char **import_vals, int import_count) {
|
||||
static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, const qn_array_t *arr,
|
||||
const char *module_qn, const char **import_vals,
|
||||
int import_count) {
|
||||
const char *filtered[CBM_SZ_256];
|
||||
uint8_t filtered_test[CBM_SZ_256];
|
||||
uint8_t filtered_lang[CBM_SZ_256];
|
||||
const uint8_t *flags = qn_test_flags(arr);
|
||||
const uint8_t *langs = qn_lang_flags(arr);
|
||||
int fcount = 0;
|
||||
for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) {
|
||||
if (is_import_reachable(arr->items[i], import_vals, import_count)) {
|
||||
if (flags) {
|
||||
filtered_test[fcount] = flags[i];
|
||||
}
|
||||
if (langs) {
|
||||
filtered_lang[fcount] = langs[i];
|
||||
}
|
||||
filtered[fcount] = arr->items[i];
|
||||
fcount++;
|
||||
}
|
||||
@@ -1429,16 +1478,16 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const
|
||||
return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count};
|
||||
}
|
||||
if (fcount > SKIP_ONE) {
|
||||
const char *best =
|
||||
best_by_import_distance(filtered, flags ? filtered_test : NULL, fcount, module_qn);
|
||||
const char *best = best_by_import_distance(r, filtered, flags ? filtered_test : NULL,
|
||||
langs ? filtered_lang : NULL, fcount, module_qn);
|
||||
if (best) {
|
||||
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount);
|
||||
return (cbm_resolution_t){best, "suffix_match", conf, fcount};
|
||||
}
|
||||
}
|
||||
/* No import-reachable — use all candidates with penalty */
|
||||
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
|
||||
arr->count, module_qn);
|
||||
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
|
||||
qn_lang_flags(arr), arr->count, module_qn);
|
||||
if (best) {
|
||||
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH * REG_HALF_PENALTY, arr->count);
|
||||
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
|
||||
@@ -1632,10 +1681,10 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
|
||||
|
||||
/* Strategy 4: multiple candidates */
|
||||
if (import_vals && import_count > 0) {
|
||||
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
|
||||
return resolve_multi_with_imports(r, arr, module_qn, import_vals, import_count);
|
||||
}
|
||||
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
|
||||
arr->count, module_qn);
|
||||
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
|
||||
qn_lang_flags(arr), arr->count, module_qn);
|
||||
if (best) {
|
||||
if (!receiver_chain_admits(callee_name, best)) {
|
||||
return empty_result();
|
||||
@@ -2011,8 +2060,8 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
|
||||
|
||||
if (fcount == 0) {
|
||||
/* No import-reachable — use originals with penalty */
|
||||
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
|
||||
arr->count, module_qn);
|
||||
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
|
||||
qn_lang_flags(arr), arr->count, module_qn);
|
||||
if (!best) {
|
||||
return no_match;
|
||||
}
|
||||
@@ -2026,8 +2075,9 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
|
||||
{fptr[0], "fuzzy", candidate_count_penalty(CONF_FUZZY_SINGLE, arr->count), arr->count},
|
||||
true};
|
||||
}
|
||||
const char *best = best_by_import_distance(
|
||||
fptr, fptr == (const char **)arr->items ? qn_test_flags(arr) : NULL, fcount, module_qn);
|
||||
const bool all = fptr == (const char **)arr->items;
|
||||
const char *best = best_by_import_distance(r, fptr, all ? qn_test_flags(arr) : NULL,
|
||||
all ? qn_lang_flags(arr) : NULL, fcount, module_qn);
|
||||
if (!best) {
|
||||
return no_match;
|
||||
}
|
||||
|
||||
+101
-5
@@ -919,9 +919,9 @@ TEST(cross_language_config_caller_drops_unique_name_too) {
|
||||
* to be settled by bucket position, i.e. by the order files were registered:
|
||||
* two indexes of one kernel tree differed by 632 CALLS edges, `dev_name`
|
||||
* landing on any of twenty same-named struct fields or on nothing at all.
|
||||
* The tie is now a function of the candidate set: the least nested
|
||||
* definition, then the smaller QN — whichever order the registry was built
|
||||
* in (O9). */
|
||||
* The tie is now a function of the candidate set: the more plainly callable
|
||||
* candidate (the function over the fields), then the smaller QN, whichever
|
||||
* order the registry was built in (O9). */
|
||||
TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
const char *cands[] = {
|
||||
"proj.drivers.media.cec.i2c.ch7322.ch7322_conn_match.dev_name", /* struct field */
|
||||
@@ -951,7 +951,7 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
ASSERT_STR_EQ(f.strategy, "suffix_match");
|
||||
ASSERT_STR_EQ(b.strategy, "suffix_match");
|
||||
|
||||
/* Equal depth: the smaller QN, from either order. */
|
||||
/* Equal kind: the smaller QN, from either order. */
|
||||
cbm_registry_t *lex_a = cbm_registry_new();
|
||||
cbm_registry_t *lex_b = cbm_registry_new();
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function");
|
||||
@@ -965,7 +965,7 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
ASSERT_STR_EQ(la.qualified_name, "proj.include.linux.scatterlist.sg_next");
|
||||
ASSERT_STR_EQ(lb.qualified_name, "proj.include.linux.scatterlist.sg_next");
|
||||
|
||||
/* Proximity still outranks depth: the sibling wins over a shallower stranger. */
|
||||
/* Proximity outranks the tie order: the sibling wins over a shallower stranger. */
|
||||
cbm_registry_t *near = cbm_registry_new();
|
||||
cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function");
|
||||
cbm_registry_add(near, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free",
|
||||
@@ -982,6 +982,99 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A call names something callable. When same-named candidates tie, a method
|
||||
* must not lose to a property or field that merely shares its name. Exposed
|
||||
* (Kotlin): `cities.selectAll().where { ... }` bound to the property
|
||||
* `AbstractQuery.where` instead of the jdbc `Query.where` function, because the
|
||||
* two QNs have the same depth and "exposed-core" sorts first; main moved 785 ->
|
||||
* 1,648 Kotlin CALLS edges onto Variable/Field targets this way. */
|
||||
TEST(registry_tie_prefers_callable_over_property) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(
|
||||
r, "where",
|
||||
"proj.exposed-core.src.main.kotlin.org.jetbrains.exposed.v1.core.AbstractQuery.where",
|
||||
"Variable");
|
||||
cbm_registry_add(r, "where",
|
||||
"proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where",
|
||||
"Method");
|
||||
/* A deeper method still beats a shallower field: kind decides before order. */
|
||||
cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field");
|
||||
cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method");
|
||||
|
||||
const char *caller =
|
||||
"proj.exposed-tests.src.test.kotlin.org.jetbrains.exposed.v1.tests.SelectTests";
|
||||
cbm_resolution_t w = cbm_registry_resolve(r, "where", caller, NULL, NULL, 0);
|
||||
cbm_resolution_t l = cbm_registry_resolve(r, "limit", caller, NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(w.qualified_name,
|
||||
"proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where");
|
||||
ASSERT_STR_EQ(l.qualified_name, "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit");
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Nesting depth says nothing about which same-named method a call means.
|
||||
* django: `Model.objects.filter(...)` from tests/ tied between
|
||||
* `QuerySet.filter` and the template `Library.filter`; "least nested wins"
|
||||
* picked Library (one segment shallower) for most of the ~5,600 `objects.X`
|
||||
* calls (QuerySet share 57 % -> 24 %). Equal-kind ties now fall to the QN
|
||||
* order alone, which is still a pure function of the candidate set (O9). */
|
||||
TEST(registry_tie_ignores_nesting_depth) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method");
|
||||
cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method");
|
||||
cbm_resolution_t f =
|
||||
cbm_registry_resolve(r, "filter", "proj.tests.lookup.tests", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(f.qualified_name, "proj.django.db.models.query.QuerySet.filter");
|
||||
ASSERT_STR_EQ(f.strategy, "suffix_match");
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* .NET reference assemblies (`src/libraries/<Asm>/ref/<Asm>.cs`) declare every
|
||||
* public member of an assembly with a `throw null` body in one shallow file.
|
||||
* A call that ties between such a stub and the implementation must reach the
|
||||
* implementation: dotnet/runtime had 14.1 % of its CALLS on ref/ stubs (4.9 %
|
||||
* in v0.11.0) once ties preferred the shallower, then the smaller ("ref" <
|
||||
* "src") QN. The preference is C#'s alone: elsewhere a `ref` directory is an
|
||||
* ordinary name. */
|
||||
TEST(registry_tie_prefers_csharp_implementation_over_ref_stub) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
/* Shallower stub vs deeper implementation (MemoryStream). */
|
||||
cbm_registry_add_lang(
|
||||
r, "MemoryStream",
|
||||
"proj.src.libraries.System.Runtime.ref.System.Runtime.MemoryStream.MemoryStream", "Method",
|
||||
CBM_LANG_CSHARP);
|
||||
cbm_registry_add_lang(
|
||||
r, "MemoryStream",
|
||||
"proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
/* Equal depth, "ref" sorts before "src" (WithCancellation). */
|
||||
cbm_registry_add_lang(r, "WithCancellation",
|
||||
"proj.src.libraries.System.Linq.Parallel.ref.System.Linq."
|
||||
"ParallelEnumerable.WithCancellation",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
cbm_registry_add_lang(r, "WithCancellation",
|
||||
"proj.src.libraries.System.Linq.Parallel.src.System.Linq."
|
||||
"ParallelEnumerable.WithCancellation",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
/* Not C#: a `ref` directory gets no special treatment. */
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA);
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA);
|
||||
|
||||
const char *caller = "proj.src.libraries.System.Security.Cryptography.src.CapiHelper";
|
||||
cbm_resolution_t ms = cbm_registry_resolve(r, "MemoryStream", caller, NULL, NULL, 0);
|
||||
cbm_resolution_t wc = cbm_registry_resolve(r, "WithCancellation", caller, NULL, NULL, 0);
|
||||
cbm_resolution_t jv = cbm_registry_resolve(r, "resolve", "proj.app.Main", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(
|
||||
ms.qualified_name,
|
||||
"proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream");
|
||||
ASSERT_STR_EQ(wc.qualified_name, "proj.src.libraries.System.Linq.Parallel.src.System.Linq."
|
||||
"ParallelEnumerable.WithCancellation");
|
||||
ASSERT_STR_EQ(jv.qualified_name, "proj.lib.ref.Resolver.resolve");
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(lang_resolution_compatible_families) {
|
||||
/* Same language and the interop families the resolvers already honour. */
|
||||
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_PYTHON, CBM_LANG_PYTHON));
|
||||
@@ -1643,6 +1736,9 @@ SUITE(registry) {
|
||||
RUN_TEST(same_module_vetoed_across_languages);
|
||||
RUN_TEST(cross_language_config_caller_drops_unique_name_too);
|
||||
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
|
||||
RUN_TEST(registry_tie_prefers_callable_over_property);
|
||||
RUN_TEST(registry_tie_ignores_nesting_depth);
|
||||
RUN_TEST(registry_tie_prefers_csharp_implementation_over_ref_stub);
|
||||
RUN_TEST(cross_language_ref_drops_go_vs_c);
|
||||
RUN_TEST(go_bare_ref_never_binds_field);
|
||||
RUN_TEST(call_onto_field_follows_language_and_shape);
|
||||
|
||||
Reference in New Issue
Block a user