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:
Martin Vogel
2026-10-10 13:31:48 +02:00
committed by GitHub
2 changed files with 186 additions and 40 deletions
+85 -35
View File
@@ -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
View File
@@ -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);