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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,63 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const
return empty_result();
}

/* Confidence for a full qualified-tail match (Strategy 3.5). A package- or
* namespace-qualified callee that uniquely matches one candidate's full tail is
* as trustworthy as a same-module hit. */
#define CONF_QUALIFIED_SUFFIX 0.90

/* When a callee is package/namespace-qualified (Foo::Bar::sub or Foo.Bar.sub),
* disambiguate among same-simple-name candidates by matching the FULL qualified
* tail against each candidate QN at a segment boundary. Returns the sole
* candidate whose QN equals or ends with ".<dotted-callee>", or NULL when zero
* or several candidates match (the caller then falls back to bare-name scoring).
*
* Fixes qualified cross-file calls collapsing onto one namespace when the bare
* symbol name is defined in several — e.g. Perl's Foo::Bar::run and Foo::Baz::run
* both reduce to "run", so the bare-name scorer would route every caller to a
* single winner. Language agnostic: callees with no separator return NULL and
* leave behavior unchanged. */
static const char *qualified_suffix_match(const qn_array_t *arr, const char *callee_name) {
/* Normalize "::" → "." so the tail composes with dotted candidate QNs. */
char dotted[CBM_SZ_512];
size_t w = 0;
for (const char *s = callee_name; *s && w + SKIP_ONE < sizeof(dotted);) {
if (s[0] == ':' && s[1] == ':') {
dotted[w++] = '.';
s += 2;
} else {
dotted[w++] = *s++;
}
}
dotted[w] = '\0';
/* Must be qualified (contain a '.') — a bare name matches every candidate
* and carries no disambiguating signal. */
if (!strchr(dotted, '.')) {
return NULL;
}
const char *match = NULL;
for (int i = 0; i < arr->count; i++) {
const char *qn = arr->items[i];
size_t qlen = strlen(qn);
if (qlen < w) {
continue;
}
const char *tail = qn + (qlen - w);
if (strcmp(tail, dotted) != 0) {
continue;
}
/* Segment boundary: tail is the whole QN or is preceded by '.'. */
if (tail != qn && tail[-1] != '.') {
continue;
}
if (match) {
return NULL; /* ambiguous — more than one qualified tail matches */
}
match = qn;
}
return match;
}

/* Strategy 3+4: Name lookup + suffix match */
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
const char *module_qn, const char **import_vals,
Expand All @@ -600,6 +657,16 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */
}

/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
* candidates by full qualified tail, before bare-name scoring collapses
* them onto a single winner. */
if (arr->count > 1) {
const char *q = qualified_suffix_match(arr, callee_name);
if (q) {
return (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
}
}

/* Strategy 3: unique name */
if (arr->count == SKIP_ONE) {
double conf = CONF_UNIQUE_NAME;
Expand Down
64 changes: 64 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,68 @@ TEST(resolve_same_module) {
PASS();
}

/* A package/namespace-qualified callee whose bare name is defined in several
* places must resolve to the package named in the call — not collapse onto a
* single winner. Regression for qualified cross-file calls (e.g. Perl
* Foo::Bar::sub()) where the same sub name exists in multiple packages. */
TEST(resolve_qualified_disambiguates_same_name) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function");
cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function");
cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function");

/* Each fully-qualified call routes to its own package. */
cbm_resolution_t a =
cbm_registry_resolve(r, "App::Alpha::save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(a.qualified_name, "proj.lib.App.Alpha.save");
ASSERT_STR_EQ(a.strategy, "qualified_suffix");

cbm_resolution_t b =
cbm_registry_resolve(r, "App::Beta::save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(b.qualified_name, "proj.lib.App.Beta.save");

cbm_resolution_t g =
cbm_registry_resolve(r, "App::Gamma::save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(g.qualified_name, "proj.lib.App.Gamma.save");

/* The dotted callee form (Go/Python/C#) disambiguates identically. */
cbm_resolution_t dotted =
cbm_registry_resolve(r, "App.Beta.save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(dotted.qualified_name, "proj.lib.App.Beta.save");
ASSERT_STR_EQ(dotted.strategy, "qualified_suffix");

/* A qualified callee whose tail matches NO candidate falls through to the
* existing bare-name scoring (never a qualified_suffix result). */
cbm_resolution_t nomatch =
cbm_registry_resolve(r, "Other::Pkg::save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_TRUE(!nomatch.strategy || strcmp(nomatch.strategy, "qualified_suffix") != 0);

/* A bare call stays ambiguous (no qualifier → no disambiguation signal). */
cbm_resolution_t bare =
cbm_registry_resolve(r, "save", "proj.lib.App.Caller", NULL, NULL, 0);
ASSERT_TRUE(!bare.strategy || strcmp(bare.strategy, "qualified_suffix") != 0);

cbm_registry_free(r);
PASS();
}

/* When two candidates share the same qualified tail, a qualified callee is
* genuinely ambiguous and must fall through to bare-name scoring rather than
* pick arbitrarily under the high-confidence qualified_suffix strategy. */
TEST(resolve_qualified_ambiguous_tail_falls_through) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function");
cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function");

/* "Foo::Bar::run" tail matches BOTH candidates → not unique → fall through. */
cbm_resolution_t res =
cbm_registry_resolve(r, "Foo::Bar::run", "proj.svcA.Caller", NULL, NULL, 0);
ASSERT_TRUE(!res.strategy || strcmp(res.strategy, "qualified_suffix") != 0);

cbm_registry_free(r);
PASS();
}

TEST(resolve_import_map) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function");
Expand Down Expand Up @@ -656,6 +718,8 @@ SUITE(registry) {
RUN_TEST(registry_no_duplicates);
/* Resolution */
RUN_TEST(resolve_same_module);
RUN_TEST(resolve_qualified_disambiguates_same_name);
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
RUN_TEST(resolve_import_map);
RUN_TEST(resolve_import_map_bare_function);
RUN_TEST(resolve_unique_name);
Expand Down
Loading