Skip to content

Commit ec4fbd3

Browse files
authored
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
2 parents 5f906a2 + 5136415 commit ec4fbd3

2 files changed

Lines changed: 186 additions & 40 deletions

File tree

‎src/pipeline/registry.c‎

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ static const uint8_t *qn_test_flags(const qn_array_t *arr) {
223223
return arr->is_test && arr->is_test_cap >= arr->count ? arr->is_test : NULL;
224224
}
225225

226+
/* The cached candidate languages, under the same coverage rule: both side
227+
* arrays grow together and share is_test_cap. */
228+
static const uint8_t *qn_lang_flags(const qn_array_t *arr) {
229+
return arr->lang && arr->is_test_cap >= arr->count ? arr->lang : NULL;
230+
}
231+
226232
/* Score a candidate for tiebreaking. Higher = better.
227233
* Layer 1: Non-test code preferred over test code (+1000)
228234
* 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
237243
return score;
238244
}
239245

240-
/* Number of '.'-separated segments in a QN: how deeply the definition is
241-
* nested (file depth + enclosing types). */
242-
static int qn_depth(const char *qn) {
243-
int depth = 1;
244-
for (const char *p = qn; *p; p++) {
245-
if (*p == '.') {
246-
depth++;
247-
}
246+
/* How plainly a candidate is something a call can target, lower = better: a
247+
* function or method; then any other definition (a class for a constructor
248+
* call, a macro, a type); then a candidate whose label is unknown; last a
249+
* variable or field, which a call reaches only when it holds a function. */
250+
enum {
251+
REG_RANK_CALLABLE = 0,
252+
REG_RANK_OTHER_DEF = 1,
253+
REG_RANK_UNKNOWN = 2,
254+
REG_RANK_DATA = 3,
255+
};
256+
257+
static int callable_rank(const cbm_registry_t *r, const char *qn) {
258+
const char *label = r ? cbm_registry_label_of(r, qn) : NULL;
259+
if (!label) {
260+
return REG_RANK_UNKNOWN;
261+
}
262+
if (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0) {
263+
return REG_RANK_CALLABLE;
264+
}
265+
if (strcmp(label, "Variable") == 0 || strcmp(label, "Field") == 0) {
266+
return REG_RANK_DATA;
248267
}
249-
return depth;
268+
return REG_RANK_OTHER_DEF;
269+
}
270+
271+
/* A .NET reference assembly (`src/libraries/<Asm>/ref/<Asm>.cs`) declares an
272+
* assembly's public surface with `throw null` bodies in one shallow file; a
273+
* call that could equally mean the implementation means the implementation.
274+
* C# only: elsewhere a `ref` directory is an ordinary name. */
275+
static bool csharp_reference_stub(const char *qn, uint8_t lang) {
276+
return lang == CBM_LANG_CSHARP && strstr(qn, ".ref.") != NULL;
250277
}
251278

252279
/* Total order among candidates that tie on candidate_score. The registry's
@@ -255,33 +282,49 @@ static int qn_depth(const char *qn) {
255282
* that order (kernel: 632 CALLS edges differed between two indexes of the
256283
* same tree, `dev_name` flipping between twenty same-named struct fields
257284
* and unresolved, `sg_set_buf` between include/linux and tools/virtio).
258-
* The rule: the least nested definition wins (a top-level function over a
259-
* same-named member two types deep, include/linux over tools/virtio/linux),
260-
* then the lexicographically smaller QN — a pure function of the candidate
261-
* set, never of the order it was built in (O9). */
262-
static bool candidate_outranks_on_tie(const char *candidate, const char *best) {
263-
int cd = qn_depth(candidate);
264-
int bd = qn_depth(best);
265-
if (cd != bd) {
266-
return cd < bd;
285+
* The rule, a pure function of the candidate set (O9):
286+
* 1. the more plainly callable candidate (`dev_name` the function over the
287+
* struct fields; Kotlin `where(...)` the method over the property);
288+
* 2. for C#, an implementation over a reference-assembly stub;
289+
* 3. the lexicographically smaller QN (include/linux over tools/virtio).
290+
* Nesting depth is deliberately not a criterion: "least nested wins" sent
291+
* django's `Model.objects.filter` to the template `Library.filter`, one
292+
* segment shallower than `QuerySet.filter` (2026-10-10 audit). */
293+
static bool candidate_outranks_on_tie(const cbm_registry_t *r, const char *candidate,
294+
uint8_t candidate_lang, const char *best, uint8_t best_lang) {
295+
int ck = callable_rank(r, candidate);
296+
int bk = callable_rank(r, best);
297+
if (ck != bk) {
298+
return ck < bk;
299+
}
300+
bool cs = csharp_reference_stub(candidate, candidate_lang);
301+
bool bs = csharp_reference_stub(best, best_lang);
302+
if (cs != bs) {
303+
return !cs;
267304
}
268305
return strcmp(candidate, best) < 0;
269306
}
270307

271308
/* Pick candidate with highest composite score (test-deprioritization + namespace
272-
* proximity). `is_test_flags` is the candidates' cached test verdicts, or NULL
273-
* when the caller has none (a filtered subset carries its own copy). */
274-
static const char *best_by_import_distance(const char **candidates, const uint8_t *is_test_flags,
309+
* proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` and
310+
* `lang_flags` are the candidates' cached test verdicts and languages, or NULL
311+
* when the caller has none (a filtered subset carries its own copies). */
312+
static const char *best_by_import_distance(const cbm_registry_t *r, const char **candidates,
313+
const uint8_t *is_test_flags, const uint8_t *lang_flags,
275314
int count, const char *module_qn) {
276315
const char *best = NULL;
316+
uint8_t best_lang = (uint8_t)CBM_LANG_COUNT;
277317
int best_score = CBM_NOT_FOUND;
278318
for (int i = 0; i < count; i++) {
279319
int score =
280320
candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1);
321+
uint8_t lang = lang_flags ? lang_flags[i] : (uint8_t)CBM_LANG_COUNT;
281322
if (score > best_score ||
282-
(score == best_score && best && candidate_outranks_on_tie(candidates[i], best))) {
323+
(score == best_score && best &&
324+
candidate_outranks_on_tie(r, candidates[i], lang, best, best_lang))) {
283325
best_score = score;
284326
best = candidates[i];
327+
best_lang = lang;
285328
}
286329
}
287330
return best;
@@ -1409,17 +1452,23 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char
14091452
}
14101453

14111454
/* Strategy 4: multiple candidates with import filtering. */
1412-
static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const char *module_qn,
1413-
const char **import_vals, int import_count) {
1455+
static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, const qn_array_t *arr,
1456+
const char *module_qn, const char **import_vals,
1457+
int import_count) {
14141458
const char *filtered[CBM_SZ_256];
14151459
uint8_t filtered_test[CBM_SZ_256];
1460+
uint8_t filtered_lang[CBM_SZ_256];
14161461
const uint8_t *flags = qn_test_flags(arr);
1462+
const uint8_t *langs = qn_lang_flags(arr);
14171463
int fcount = 0;
14181464
for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) {
14191465
if (is_import_reachable(arr->items[i], import_vals, import_count)) {
14201466
if (flags) {
14211467
filtered_test[fcount] = flags[i];
14221468
}
1469+
if (langs) {
1470+
filtered_lang[fcount] = langs[i];
1471+
}
14231472
filtered[fcount] = arr->items[i];
14241473
fcount++;
14251474
}
@@ -1429,16 +1478,16 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const
14291478
return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count};
14301479
}
14311480
if (fcount > SKIP_ONE) {
1432-
const char *best =
1433-
best_by_import_distance(filtered, flags ? filtered_test : NULL, fcount, module_qn);
1481+
const char *best = best_by_import_distance(r, filtered, flags ? filtered_test : NULL,
1482+
langs ? filtered_lang : NULL, fcount, module_qn);
14341483
if (best) {
14351484
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount);
14361485
return (cbm_resolution_t){best, "suffix_match", conf, fcount};
14371486
}
14381487
}
14391488
/* No import-reachable — use all candidates with penalty */
1440-
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
1441-
arr->count, module_qn);
1489+
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
1490+
qn_lang_flags(arr), arr->count, module_qn);
14421491
if (best) {
14431492
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH * REG_HALF_PENALTY, arr->count);
14441493
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
16321681

16331682
/* Strategy 4: multiple candidates */
16341683
if (import_vals && import_count > 0) {
1635-
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
1684+
return resolve_multi_with_imports(r, arr, module_qn, import_vals, import_count);
16361685
}
1637-
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
1638-
arr->count, module_qn);
1686+
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
1687+
qn_lang_flags(arr), arr->count, module_qn);
16391688
if (best) {
16401689
if (!receiver_chain_admits(callee_name, best)) {
16411690
return empty_result();
@@ -2011,8 +2060,8 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
20112060

20122061
if (fcount == 0) {
20132062
/* No import-reachable — use originals with penalty */
2014-
const char *best = best_by_import_distance((const char **)arr->items, qn_test_flags(arr),
2015-
arr->count, module_qn);
2063+
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
2064+
qn_lang_flags(arr), arr->count, module_qn);
20162065
if (!best) {
20172066
return no_match;
20182067
}
@@ -2026,8 +2075,9 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
20262075
{fptr[0], "fuzzy", candidate_count_penalty(CONF_FUZZY_SINGLE, arr->count), arr->count},
20272076
true};
20282077
}
2029-
const char *best = best_by_import_distance(
2030-
fptr, fptr == (const char **)arr->items ? qn_test_flags(arr) : NULL, fcount, module_qn);
2078+
const bool all = fptr == (const char **)arr->items;
2079+
const char *best = best_by_import_distance(r, fptr, all ? qn_test_flags(arr) : NULL,
2080+
all ? qn_lang_flags(arr) : NULL, fcount, module_qn);
20312081
if (!best) {
20322082
return no_match;
20332083
}

‎tests/test_registry.c‎

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -919,9 +919,9 @@ TEST(cross_language_config_caller_drops_unique_name_too) {
919919
* to be settled by bucket position, i.e. by the order files were registered:
920920
* two indexes of one kernel tree differed by 632 CALLS edges, `dev_name`
921921
* landing on any of twenty same-named struct fields or on nothing at all.
922-
* The tie is now a function of the candidate set: the least nested
923-
* definition, then the smaller QN — whichever order the registry was built
924-
* in (O9). */
922+
* The tie is now a function of the candidate set: the more plainly callable
923+
* candidate (the function over the fields), then the smaller QN, whichever
924+
* order the registry was built in (O9). */
925925
TEST(registry_tie_break_is_independent_of_registration_order) {
926926
const char *cands[] = {
927927
"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) {
951951
ASSERT_STR_EQ(f.strategy, "suffix_match");
952952
ASSERT_STR_EQ(b.strategy, "suffix_match");
953953

954-
/* Equal depth: the smaller QN, from either order. */
954+
/* Equal kind: the smaller QN, from either order. */
955955
cbm_registry_t *lex_a = cbm_registry_new();
956956
cbm_registry_t *lex_b = cbm_registry_new();
957957
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) {
965965
ASSERT_STR_EQ(la.qualified_name, "proj.include.linux.scatterlist.sg_next");
966966
ASSERT_STR_EQ(lb.qualified_name, "proj.include.linux.scatterlist.sg_next");
967967

968-
/* Proximity still outranks depth: the sibling wins over a shallower stranger. */
968+
/* Proximity outranks the tie order: the sibling wins over a shallower stranger. */
969969
cbm_registry_t *near = cbm_registry_new();
970970
cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function");
971971
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) {
982982
PASS();
983983
}
984984

985+
/* A call names something callable. When same-named candidates tie, a method
986+
* must not lose to a property or field that merely shares its name. Exposed
987+
* (Kotlin): `cities.selectAll().where { ... }` bound to the property
988+
* `AbstractQuery.where` instead of the jdbc `Query.where` function, because the
989+
* two QNs have the same depth and "exposed-core" sorts first; main moved 785 ->
990+
* 1,648 Kotlin CALLS edges onto Variable/Field targets this way. */
991+
TEST(registry_tie_prefers_callable_over_property) {
992+
cbm_registry_t *r = cbm_registry_new();
993+
cbm_registry_add(
994+
r, "where",
995+
"proj.exposed-core.src.main.kotlin.org.jetbrains.exposed.v1.core.AbstractQuery.where",
996+
"Variable");
997+
cbm_registry_add(r, "where",
998+
"proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where",
999+
"Method");
1000+
/* A deeper method still beats a shallower field: kind decides before order. */
1001+
cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field");
1002+
cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method");
1003+
1004+
const char *caller =
1005+
"proj.exposed-tests.src.test.kotlin.org.jetbrains.exposed.v1.tests.SelectTests";
1006+
cbm_resolution_t w = cbm_registry_resolve(r, "where", caller, NULL, NULL, 0);
1007+
cbm_resolution_t l = cbm_registry_resolve(r, "limit", caller, NULL, NULL, 0);
1008+
ASSERT_STR_EQ(w.qualified_name,
1009+
"proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where");
1010+
ASSERT_STR_EQ(l.qualified_name, "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit");
1011+
cbm_registry_free(r);
1012+
PASS();
1013+
}
1014+
1015+
/* Nesting depth says nothing about which same-named method a call means.
1016+
* django: `Model.objects.filter(...)` from tests/ tied between
1017+
* `QuerySet.filter` and the template `Library.filter`; "least nested wins"
1018+
* picked Library (one segment shallower) for most of the ~5,600 `objects.X`
1019+
* calls (QuerySet share 57 % -> 24 %). Equal-kind ties now fall to the QN
1020+
* order alone, which is still a pure function of the candidate set (O9). */
1021+
TEST(registry_tie_ignores_nesting_depth) {
1022+
cbm_registry_t *r = cbm_registry_new();
1023+
cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method");
1024+
cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method");
1025+
cbm_resolution_t f =
1026+
cbm_registry_resolve(r, "filter", "proj.tests.lookup.tests", NULL, NULL, 0);
1027+
ASSERT_STR_EQ(f.qualified_name, "proj.django.db.models.query.QuerySet.filter");
1028+
ASSERT_STR_EQ(f.strategy, "suffix_match");
1029+
cbm_registry_free(r);
1030+
PASS();
1031+
}
1032+
1033+
/* .NET reference assemblies (`src/libraries/<Asm>/ref/<Asm>.cs`) declare every
1034+
* public member of an assembly with a `throw null` body in one shallow file.
1035+
* A call that ties between such a stub and the implementation must reach the
1036+
* implementation: dotnet/runtime had 14.1 % of its CALLS on ref/ stubs (4.9 %
1037+
* in v0.11.0) once ties preferred the shallower, then the smaller ("ref" <
1038+
* "src") QN. The preference is C#'s alone: elsewhere a `ref` directory is an
1039+
* ordinary name. */
1040+
TEST(registry_tie_prefers_csharp_implementation_over_ref_stub) {
1041+
cbm_registry_t *r = cbm_registry_new();
1042+
/* Shallower stub vs deeper implementation (MemoryStream). */
1043+
cbm_registry_add_lang(
1044+
r, "MemoryStream",
1045+
"proj.src.libraries.System.Runtime.ref.System.Runtime.MemoryStream.MemoryStream", "Method",
1046+
CBM_LANG_CSHARP);
1047+
cbm_registry_add_lang(
1048+
r, "MemoryStream",
1049+
"proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream",
1050+
"Method", CBM_LANG_CSHARP);
1051+
/* Equal depth, "ref" sorts before "src" (WithCancellation). */
1052+
cbm_registry_add_lang(r, "WithCancellation",
1053+
"proj.src.libraries.System.Linq.Parallel.ref.System.Linq."
1054+
"ParallelEnumerable.WithCancellation",
1055+
"Method", CBM_LANG_CSHARP);
1056+
cbm_registry_add_lang(r, "WithCancellation",
1057+
"proj.src.libraries.System.Linq.Parallel.src.System.Linq."
1058+
"ParallelEnumerable.WithCancellation",
1059+
"Method", CBM_LANG_CSHARP);
1060+
/* Not C#: a `ref` directory gets no special treatment. */
1061+
cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA);
1062+
cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA);
1063+
1064+
const char *caller = "proj.src.libraries.System.Security.Cryptography.src.CapiHelper";
1065+
cbm_resolution_t ms = cbm_registry_resolve(r, "MemoryStream", caller, NULL, NULL, 0);
1066+
cbm_resolution_t wc = cbm_registry_resolve(r, "WithCancellation", caller, NULL, NULL, 0);
1067+
cbm_resolution_t jv = cbm_registry_resolve(r, "resolve", "proj.app.Main", NULL, NULL, 0);
1068+
ASSERT_STR_EQ(
1069+
ms.qualified_name,
1070+
"proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream");
1071+
ASSERT_STR_EQ(wc.qualified_name, "proj.src.libraries.System.Linq.Parallel.src.System.Linq."
1072+
"ParallelEnumerable.WithCancellation");
1073+
ASSERT_STR_EQ(jv.qualified_name, "proj.lib.ref.Resolver.resolve");
1074+
cbm_registry_free(r);
1075+
PASS();
1076+
}
1077+
9851078
TEST(lang_resolution_compatible_families) {
9861079
/* Same language and the interop families the resolvers already honour. */
9871080
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_PYTHON, CBM_LANG_PYTHON));
@@ -1643,6 +1736,9 @@ SUITE(registry) {
16431736
RUN_TEST(same_module_vetoed_across_languages);
16441737
RUN_TEST(cross_language_config_caller_drops_unique_name_too);
16451738
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
1739+
RUN_TEST(registry_tie_prefers_callable_over_property);
1740+
RUN_TEST(registry_tie_ignores_nesting_depth);
1741+
RUN_TEST(registry_tie_prefers_csharp_implementation_over_ref_stub);
16461742
RUN_TEST(cross_language_ref_drops_go_vs_c);
16471743
RUN_TEST(go_bare_ref_never_binds_field);
16481744
RUN_TEST(call_onto_field_follows_language_and_shape);

0 commit comments

Comments
 (0)