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
149 changes: 127 additions & 22 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -605,20 +605,48 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char *
return empty_result();
}

static bool is_same_module_receiver(const char *prefix, const char *module_qn) {
if (!prefix || !prefix[0]) {
return true; /* Bare names are always candidates for same-module resolution */
}
/* 1. Check known self-receivers */
static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL};
for (int i = 0; self_receivers[i]; i++) {
if (strcmp(prefix, self_receivers[i]) == 0) {
return true;
}
}
/* 2. Check if prefix matches the module name or its last segment (namespace qualified) */
size_t plen = strlen(prefix);
size_t mlen = strlen(module_qn);
if (mlen == plen && strcmp(module_qn, prefix) == 0) {
return true;
}
if (mlen > plen && module_qn[mlen - plen - 1] == '.' &&
strcmp(module_qn + (mlen - plen), prefix) == 0) {
return true;
}
return false;
}

/* Strategy 2: Same-module match */
static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name,
const char *suffix, const char *module_qn) {
const char *prefix, const char *suffix,
const char *module_qn) {
char candidate[CBM_SZ_512];
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, callee_name);
const char *stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
}
if (suffix && suffix[0]) {
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
if (is_same_module_receiver(prefix, module_qn)) {
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE,
REG_RESOLVED};
}
}
}
return empty_result();
Expand Down Expand Up @@ -710,6 +738,66 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
}
return match;
}
static bool qn_ends_with_qualified(const char *qn, const char *callee_name) {
char dotted[CBM_SZ_512];
size_t w = 0;
for (const char *s = callee_name; *s && w + 1 < sizeof(dotted);) {
if (s[0] == ':' && s[1] == ':') {
dotted[w++] = '.';
s += 2;
} else {
dotted[w++] = *s++;
}
}
dotted[w] = '\0';

size_t qlen = strlen(qn);
if (qlen < w) {
return false;
}
const char *tail = qn + (qlen - w);
if (strcmp(tail, dotted) != 0) {
return false;
}
if (tail != qn && tail[-1] != '.') {
return false;
}
return true;
}
static bool is_type_like_label(const char *label) {
if (!label) {
return false;
}
return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 ||
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
}

static bool is_candidate_method(const cbm_registry_t *r, const char *qn) {
const char *label = cbm_registry_label_of(r, qn);
if (label && strcmp(label, "Method") == 0) {
return true;
}

char parent_qn[CBM_SZ_512];
size_t len = strlen(qn);
if (len >= sizeof(parent_qn)) {
return false;
}
strcpy(parent_qn, qn);
char *last_dot = strrchr(parent_qn, '.');
if (!last_dot) {
return false;
}
*last_dot = '\0';

const char *parent_label = cbm_registry_label_of(r, parent_qn);
return is_type_like_label(parent_label);
}

static bool is_qualified_callee(const char *callee_name) {
return strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL;
}

/* Strategy 3+4: Name lookup + suffix match */
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
Expand All @@ -724,36 +812,53 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */
}

cbm_resolution_t res = empty_result();

/* 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};
res = (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;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
conf *= DEFAULT_CONFIDENCE;
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 3: unique name */
if (arr->count == 1) {
double conf = CONF_UNIQUE_NAME;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
conf *= DEFAULT_CONFIDENCE;
}
res = (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
}
return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
}

/* Strategy 4: multiple candidates */
if (import_vals && import_count > 0) {
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 4: multiple candidates */
if (import_vals && import_count > 0) {
res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
} else {
const char *best =
best_by_import_distance((const char **)arr->items, arr->count, module_qn);
if (best) {
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
res = (cbm_resolution_t){best, "suffix_match", conf, arr->count};
}
}
}
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
if (best) {
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};

if (res.qualified_name && is_qualified_callee(callee_name)) {
if (!is_candidate_method(r, res.qualified_name)) {
if (!qn_ends_with_qualified(res.qualified_name, callee_name)) {
return empty_result();
}
}
}
return empty_result();

return res;
}

cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name,
Expand Down Expand Up @@ -806,7 +911,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count);
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 2: same module */
res = resolve_same_module(r, callee_name, suffix, module_qn);
res = resolve_same_module(r, callee_name, prefix, suffix, module_qn);
}
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 3+4: name lookup */
Expand Down
31 changes: 31 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,36 @@ TEST(resolve_same_module) {
PASS();
}

TEST(resolve_same_module_only_on_self_receiver) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "get", "proj.pkg.service.get", "Function");

/* Dotted call with unrelated receiver (e.g. "axios.get") -> should NOT resolve */
cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_TRUE(res1.qualified_name == NULL || res1.qualified_name[0] == '\0');

/* Dotted call with delegation pattern (e.g. "_get_store().get") -> should NOT resolve */
cbm_resolution_t res2 = cbm_registry_resolve(r, "_get_store().get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_TRUE(res2.qualified_name == NULL || res2.qualified_name[0] == '\0');

/* Dotted call with self-receiver (e.g. "self.get") -> should resolve */
cbm_resolution_t res3 = cbm_registry_resolve(r, "self.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res3.qualified_name, "proj.pkg.service.get");
ASSERT_STR_EQ(res3.strategy, "same_module");

/* Dotted call with exact module name prefix (e.g. "proj.pkg.service.get") -> should resolve */
cbm_resolution_t res4 = cbm_registry_resolve(r, "proj.pkg.service.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res4.qualified_name, "proj.pkg.service.get");

/* Dotted call with namespace/module last segment (e.g. "service.get") -> should resolve */
cbm_resolution_t res5 = cbm_registry_resolve(r, "service.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res5.qualified_name, "proj.pkg.service.get");
ASSERT_STR_EQ(res5.strategy, "same_module");

cbm_registry_free(r);
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
Expand Down Expand Up @@ -772,6 +802,7 @@ SUITE(registry) {
RUN_TEST(registry_no_duplicates);
/* Resolution */
RUN_TEST(resolve_same_module);
RUN_TEST(resolve_same_module_only_on_self_receiver);
RUN_TEST(resolve_qualified_disambiguates_same_name);
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
RUN_TEST(resolve_import_map);
Expand Down
Loading