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
130 changes: 112 additions & 18 deletions internal/cbm/lsp/c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2714,8 +2714,17 @@ static void c_neg_memo_free(CLSPContext *ctx) {
ctx->neg_memo_count = 0;
}

/* Match a module boundary, not a textual prefix (mod != mod_extra). */
static bool c_type_in_module(const char *qn, const char *module) {
if (!module || !module[0])
return false;
size_t len = strlen(module);
return strncmp(qn, module, len) == 0 && qn[len] == '.';
}

static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const char *type_qn,
const char *member_name, int depth) {
const char *member_name, int depth,
bool *ambiguous_out) {
if (!type_qn || !member_name)
return NULL;
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH)
Expand All @@ -2730,10 +2739,10 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch

// Negative-lookup memo (depth==0, shared read-only registry only). A recorded
// hit means this exact (type_qn, member) already failed the whole cascade. Under
// the sealed Tier-2 registry the module-prefix (below), base-class and short-name
// cascades are pure, immutable functions of (type_qn, registry), so they are
// provably still NULL and can be skipped. Only the SCOPE-ALIAS path is
// context-dependent, so it is still evaluated below before we trust the memo.
// the sealed Tier-2 registry and this file's fixed module/include mappings,
// the module-prefix, base-class and short-name cascades remain unchanged.
// Only the SCOPE-ALIAS path can change during the walk, so it is still
// evaluated below before we trust the memo.
// The direct lookup above already served as the collision/staleness verification.
bool neg_memo_hit = false;
uint64_t neg_h = 0;
Expand Down Expand Up @@ -2764,7 +2773,7 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
if (underlying && !cbm_type_is_unknown(underlying)) {
const char *alias_target_qn = type_to_qn(underlying);
if (alias_target_qn) {
f = c_lookup_member_depth(ctx, alias_target_qn, member_name, depth + 1);
f = c_lookup_member_depth(ctx, alias_target_qn, member_name, depth + 1, NULL);
if (f)
return f;
}
Expand All @@ -2773,7 +2782,7 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
}

// Memo hit and the scope-alias path also missed: the remaining base-class and
// short-name cascades are registry-only and provably NULL → skip them (this is
// short-name cascades are unchanged for this file and still NULL → skip them (this is
// where the O(type_count) short-name scan is avoided). Idempotent re-insert is
// unnecessary since neg_h is already present.
if (neg_memo_hit)
Expand All @@ -2791,15 +2800,15 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
if (rt) {
// Alias chain
if (rt->alias_of) {
f = c_lookup_member_depth(ctx, rt->alias_of, member_name, depth + 1);
f = c_lookup_member_depth(ctx, rt->alias_of, member_name, depth + 1, NULL);
if (f)
return f;
}

// Base classes (embedded_types stores base class QNs)
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
f = c_lookup_member_depth(ctx, rt->embedded_types[i], member_name, depth + 1);
f = c_lookup_member_depth(ctx, rt->embedded_types[i], member_name, depth + 1, NULL);
if (f)
return f;
}
Expand All @@ -2812,13 +2821,21 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
* file-scoped "<module>.Logger" or the bare "Logger" (e.g. the return type of
* a namespace-scoped factory used outside that namespace). Resolve by the
* SHORT name (last segment) against the registry and retry with the full QN.
* Reached only after the direct/module/alias/base lookups all miss; prefers
* an in-module match. Mirrors the C# short-name type fallback. */
* Reached only after the direct/module/alias/base lookups all miss. Prefer
* the current module, then literal relative includes, then a globally unique
* candidate. Equal-rank distinct QNs stay unresolved, independent of order. */
if (depth == 0 && ctx->registry) {
const char *dot = strrchr(type_qn, '.');
const char *shortn = dot ? dot + 1 : type_qn;
size_t slen = strlen(shortn);
const char *type_suffix = type_qn;
if (c_type_in_module(type_qn, ctx->module_qn))
type_suffix += strlen(ctx->module_qn) + 1;
size_t suffix_len = strlen(type_suffix);
const char *best_qn = NULL;
int best_rank = -1;
bool ambiguous = false;
bool excluded_scope = false;
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, shortn, &it);
int i;
Expand All @@ -2835,13 +2852,41 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
if (strcmp(q, type_qn) == 0) {
continue; // already tried as-is above
}
best_qn = q;
if (ctx->module_qn && strncmp(q, ctx->module_qn, strlen(ctx->module_qn)) == 0) {
break; // prefer a match in the current module
// Preserve namespace qualification when comparing included types.
// Preserve the legacy cross-file short-name cascade without an
// include map, where a QN may carry a different file's prefix.
if (ctx->include_count > 0 && suffix_len > slen &&
(qlen <= suffix_len || q[qlen - suffix_len - 1] != '.' ||
strcmp(q + qlen - suffix_len, type_suffix) != 0)) {
excluded_scope = true;
continue;
}
int rank = c_type_in_module(q, ctx->module_qn) ? 4 : 0;
for (int j = 0; j < ctx->include_count; j++) {
const char *included = ctx->include_ns_qns[j];
if (c_type_in_module(q, included)) {
// A directly named type in the header outranks its
// namespaced short-name homonyms.
int included_rank = strcmp(q + strlen(included) + 1, type_suffix) == 0 ? 3 : 1;
if (included_rank > rank)
rank = included_rank;
}
}
if (rank > best_rank) {
best_qn = q;
best_rank = rank;
ambiguous = false;
} else if (rank == best_rank && strcmp(best_qn, q) != 0) {
ambiguous = true;
}
}
if (ambiguous || (!best_qn && excluded_scope)) {
if (ambiguous_out)
*ambiguous_out = true;
return NULL; // Do not memoize: callers need the ambiguity diagnostic.
}
if (best_qn) {
f = c_lookup_member_depth(ctx, best_qn, member_name, depth + 1);
f = c_lookup_member_depth(ctx, best_qn, member_name, depth + 1, NULL);
if (f) {
return f;
}
Expand All @@ -2857,7 +2902,7 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch

const CBMRegisteredFunc *c_lookup_member(CLSPContext *ctx, const char *type_qn,
const char *member_name) {
return c_lookup_member_depth(ctx, type_qn, member_name, 0);
return c_lookup_member_depth(ctx, type_qn, member_name, 0, NULL);
}

// True if any BASE class of type_qn (not type_qn itself) declares member_name —
Expand Down Expand Up @@ -3990,9 +4035,16 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) {
// Use type-aware overload scoring
const CBMRegisteredFunc *method = cbm_registry_lookup_method_by_types(
ctx->registry, type_qn, field_name, arg_types, arg_count);
// Fall back to c_lookup_member for base class traversal
// Keep ambiguous receiver identity distinct from an ordinary miss.
bool ambiguous = false;
if (!method)
method = c_lookup_member(ctx, type_qn, field_name);
method =
c_lookup_member_depth(ctx, type_qn, field_name, 0, &ambiguous);
if (!method && ambiguous) {
c_emit_resolved_call_orig_at(ctx, field_name, NULL,
"lsp_ambiguous_receiver", 0.0f, node);
goto recurse;
}
if (ctx->debug)
fprintf(stderr, " [clsp] member call result: %s\n",
method ? method->qualified_name : "NULL");
Expand Down Expand Up @@ -5425,6 +5477,45 @@ static void c_process_class(CLSPContext *ctx, TSNode class_node) {
// Process file: top-level walk
// ============================================================================

/* Only top-level literal quoted includes with an unambiguous relative spelling.
* Module QNs omit the extension. Do not guess include search paths, macros,
* parent traversal, conditional includes, or dotted path components. */
static void c_collect_relative_include(CLSPContext *ctx, TSNode node) {
if (!ctx->module_qn || strcmp(ts_node_type(node), "preproc_include") != 0)
return;
TSNode path_node = ts_node_child_by_field_name(node, "path", 4);
if (ts_node_is_null(path_node) || strcmp(ts_node_type(path_node), "string_literal") != 0)
return;
char *path = c_node_text(ctx, path_node);
size_t len = path ? strlen(path) : 0;
if (len < 4 || path[0] != '"' || path[len - 1] != '"')
return;
path[len - 1] = '\0';
char *relative = path + 1;
if (relative[0] == '/' || relative[0] == '\\' || strchr(relative, ':'))
return;
char *ext = strrchr(relative, '.');
if (!ext || (strcmp(ext, ".h") != 0 && strcmp(ext, ".hpp") != 0 && strcmp(ext, ".hh") != 0 &&
strcmp(ext, ".hxx") != 0))
return;
*ext = '\0';
if (!relative[0] || strchr(relative, '.'))
return;
for (char *p = relative; *p; p++) {
if (*p == '/' || *p == '\\') {
if (p == relative || p[-1] == '.' || !p[1])
return;
*p = '.';
}
}
const char *dot = strrchr(ctx->module_qn, '.');
if (!dot)
return;
const char *module = cbm_arena_sprintf(ctx->arena, "%.*s.%s", (int)(dot - ctx->module_qn),
ctx->module_qn, relative);
c_lsp_add_include(ctx, relative, module);
}

void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
if (ts_node_is_null(root))
return;
Expand All @@ -5436,6 +5527,9 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
TSNode child; // Hoisted: prevents ASan stack-use-after-scope between passes
TSNode inner;

for (uint32_t i = 0; i < kn; i++)
c_collect_relative_include(ctx, kids[i]);

// Pass 1: process using declarations and global variables
for (uint32_t i = 0; i < kn; i++) {
child = kids[i];
Expand Down
38 changes: 38 additions & 0 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,41 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs
* directly across N files (test_incremental.c saw 3.5 GB peak on a
* 1100-file repo before this fix). Output gets copied into the file's own
* arena and merged into result->resolved_calls. */
/* Preserve a negative receiver-identity decision at the exact raw/preprocessed
* occurrence. Both sequential and fused-parallel CALLS passes already honor
* requires_lsp_resolution; unrelated unresolved calls keep their fallback. */
static void pxc_require_unambiguous_receivers(CBMFileResult *result,
const CBMResolvedCallArray *out) {
CBMArena keys;
cbm_arena_init(&keys);
CBMHashTable *blocked = NULL;
for (int i = 0; i < out->count; i++) {
const CBMResolvedCall *r = &out->items[i];
if (!r->strategy || strcmp(r->strategy, "lsp_ambiguous_receiver") != 0 ||
r->kind != CBM_RESOLVED_INVOCATION || !r->caller_qn ||
r->site_end_byte <= r->site_start_byte)
continue;
if (!blocked)
blocked = cbm_ht_create((uint32_t)(out->count + 1));
char *key = cbm_arena_sprintf(&keys, "%s:%u:%u:%u", r->caller_qn, r->site_start_byte,
r->site_end_byte, (unsigned)r->source_origin);
cbm_ht_set(blocked, key, (void *)1);
}
for (int i = 0; blocked && i < result->calls.count; i++) {
CBMCall *call = &result->calls.items[i];
if (!call->enclosing_func_qn || call->site_end_byte <= call->site_start_byte)
continue;
char *key =
cbm_arena_sprintf(&keys, "%s:%u:%u:%u", call->enclosing_func_qn, call->site_start_byte,
call->site_end_byte, (unsigned)call->source_origin);
if (cbm_ht_get(blocked, key))
call->requires_lsp_resolution = true;
}
if (blocked)
cbm_ht_free(blocked);
cbm_arena_destroy(&keys);
}

void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count) {
Expand Down Expand Up @@ -1337,6 +1372,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
break;
}

pxc_require_unambiguous_receivers(r, &out);
pxc_append_results(&r->arena, &r->resolved_calls, &out);
pxc_append_synthetic_calls(&r->arena, &r->calls, &synthetic_calls);
pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch);
Expand All @@ -1356,6 +1392,7 @@ void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, co
cbm_run_ts_lsp_cross(&scratch, source, source_len, module_qn, js_mode, jsx_mode, dts_mode, defs,
def_count, imp_names, imp_qns, imp_count, r->cached_tree, &out);

pxc_require_unambiguous_receivers(r, &out);
pxc_append_results(&r->arena, &r->resolved_calls, &out);
pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch);
}
Expand Down Expand Up @@ -1517,6 +1554,7 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
break;
}
if (used_prebuilt) {
pxc_require_unambiguous_receivers(result, &out);
pxc_append_results(&result->arena, &result->resolved_calls, &out);
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
}
Expand Down
50 changes: 50 additions & 0 deletions tests/test_c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -15677,6 +15677,55 @@ TEST(registry_short_name_indexes) {
PASS();
}

/* Synthetic duplicate receiver candidates: include provenance, module
* boundaries, explicit namespace and insertion order must all be respected. */
TEST(clsp_duplicate_receiver_include_selection) {
for (int reverse = 0; reverse < 2; reverse++) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &arena);
const char *qns[] = {
"test.fixture_latch.FixtureLatch", "test.mirror.fixture_latch.FixtureLatch",
"test.fixture_latch_extra.FixtureLatch", "test.fixture_latch.North.FixtureLatch",
"test.fixture_latch.South.FixtureLatch"};
for (int n = 0; n < 5; n++) {
int i = reverse ? 4 - n : n;
CBMRegisteredType t = {0};
t.qualified_name = qns[i];
t.short_name = "FixtureLatch";
cbm_registry_add_type(&reg, t);
CBMRegisteredFunc f = {0};
f.qualified_name = cbm_arena_sprintf(&arena, "%s.Acquire", qns[i]);
f.receiver_type = qns[i];
f.short_name = "Acquire";
cbm_registry_add_func(&reg, f);
}
cbm_registry_finalize(&reg);
CLSPContext ctx;
CBMResolvedCallArray out = {0};
c_lsp_init(&ctx, &arena, "", 0, &reg, "test.caller", true, &out);
ASSERT_NULL(c_lookup_member(&ctx, "test.caller.FixtureLatch", "Acquire"));
c_lsp_add_include(&ctx, "fixture_latch.h", "test.fixture_latch");
const CBMRegisteredFunc *f = c_lookup_member(&ctx, "North.FixtureLatch", "Acquire");
ASSERT_NOT_NULL(f);
ASSERT_STR_EQ(f->qualified_name, "test.fixture_latch.North.FixtureLatch.Acquire");
f = c_lookup_member(&ctx, "test.caller.South.FixtureLatch", "Acquire");
ASSERT_NOT_NULL(f);
ASSERT_STR_EQ(f->qualified_name, "test.fixture_latch.South.FixtureLatch.Acquire");
ASSERT_NULL(c_lookup_member(&ctx, "Absent.FixtureLatch", "Acquire"));
// Directly declared FixtureLatch outranks namespaced homonyms and the
// textual-prefix sibling fixture_latch_extra.
f = c_lookup_member(&ctx, "test.caller.FixtureLatch", "Acquire");
ASSERT_NOT_NULL(f);
ASSERT_STR_EQ(f->qualified_name, "test.fixture_latch.FixtureLatch.Acquire");
c_lsp_add_include(&ctx, "mirror/fixture_latch.h", "test.mirror.fixture_latch");
ASSERT_NULL(c_lookup_member(&ctx, "test.caller.FixtureLatch", "Acquire"));
cbm_arena_destroy(&arena);
}
PASS();
}

/* ── Suite ─────────────────────────────────────────────────────── */

/* C++ sibling guard: the same shared-registry read-only invariant for the
Expand Down Expand Up @@ -16386,6 +16435,7 @@ TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite) {
}

SUITE(c_lsp) {
RUN_TEST(clsp_duplicate_receiver_include_selection);
RUN_TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences);
RUN_TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target);
RUN_TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences);
Expand Down
Loading
Loading