From 09c0762938c24d71ac00aeb12de0f3d5c2faa1ec Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Tue, 14 Jul 2026 03:07:48 -0400 Subject: [PATCH] fix(kotlin): resolve default-package project calls Index Java and Kotlin definitions with an internal key in src/pipeline/pass_lsp_cross.c so cbm_pxc_filter_defs_for_file() retains sibling-file declarations when no package header exists. The hash table rejects empty keys, which previously removed Algo.kt::maxOf before cross-file resolution. Initialize cbm_run_kotlin_lsp_cross() with the file module QN so default-package caller identities match extracted graph QNs such as .Main.run. In pxc_append_results(), index resolutions by caller and callee short name and let an equal-confidence project-wide result replace the earlier per-file guess; this changes kotlin.comparisons.maxOf to .Algo.maxOf without adding O(n^2) scans. ASan/UBSan validation: lsp_resolution_probe passed 83/83, kotlin_lsp passed 79/79, java_lsp passed 379/379, and parallel passed 36/36. The focused lrp_kotlin_s7_generic debug run resolved one CALLS edge from .Main.run to .Algo.maxOf. git diff --check passed. Signed-off-by: Andrew Hundt --- internal/cbm/lsp/kotlin_lsp.c | 41 +++++++++++++----------- src/pipeline/pass_lsp_cross.c | 59 +++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/internal/cbm/lsp/kotlin_lsp.c b/internal/cbm/lsp/kotlin_lsp.c index 5f959a59a..067668c90 100644 --- a/internal/cbm/lsp/kotlin_lsp.c +++ b/internal/cbm/lsp/kotlin_lsp.c @@ -642,26 +642,22 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name } } - /* Default imports */ - const char *via_default = kt_resolve_in_default_imports(ctx, name, CBM_KT_USE_FUNCTION); - if (via_default) { - return via_default; - } - - /* Cross-file sole-definer fallback. In the default package (no `package` + /* Cross-file sole-definer fallback. Kotlin declarations in the current + * project/package shadow default imports, so this lookup must run before + * kotlin.*, java.*, and other implicit-import packages. In the default + * package (no `package` * declaration) a top-level `double()` in Util.kt is callable bare from * Main.kt, but its registered QN embeds the defining file's path * (".Util.double") which the caller can't reconstruct. When the - * project-wide registry holds EXACTLY ONE top-level function (receiver_type - * == NULL) whose short name matches, resolve to it. Bounded to a single - * candidate so an ambiguous name (>1 definer) is left unresolved — sound, - * mirroring the registry's "unique_name" strategy. Runs only after the - * package/import/bare/default-import lookups miss, so it never overrides a - * more specific match; in the per-file pass the registry holds just this - * file's defs, so the candidate is the file's own sole top-level fun. */ + * project-wide registry holds EXACTLY ONE project top-level function + * (receiver_type == NULL) whose short name matches, resolve to it. Bounded + * to a single candidate so an ambiguous name (>1 definer) is unresolved. + * Filtering on project_name prevents a same-named stdlib function such as + * kotlin.comparisons.maxOf from making the project declaration ambiguous. */ if (ctx->registry && ctx->registry->funcs) { const char *only_qn = NULL; int matches = 0; + size_t project_len = ctx->project_name ? strlen(ctx->project_name) : 0; for (int i = 0; i < ctx->registry->func_count && matches < 2; i++) { const CBMRegisteredFunc *f = &ctx->registry->funcs[i]; if (!f->qualified_name || !f->short_name) { @@ -670,7 +666,10 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name if (f->receiver_type) { /* method / extension — not a bare top-level fun */ continue; } - if (strcmp(f->short_name, name) == 0) { + bool project_def = project_len > 0 && + strncmp(f->qualified_name, ctx->project_name, project_len) == 0 && + f->qualified_name[project_len] == '.'; + if (project_def && strcmp(f->short_name, name) == 0) { only_qn = f->qualified_name; matches++; } @@ -680,7 +679,9 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name } } - return NULL; + /* Default imports are the final function-name fallback after explicit, + * same-package, wildcard, and unique project declarations. */ + return kt_resolve_in_default_imports(ctx, name, CBM_KT_USE_FUNCTION); } static const char *kt_resolve_in_default_imports(KotlinLSPContext *ctx, const char *name, @@ -4346,8 +4347,12 @@ void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_le } KotlinLSPContext ctx; - kotlin_lsp_init(&ctx, arena, source, source_len, ®, "", module_qn ? module_qn : "", - project_name, /*rel_path=*/NULL, out); + /* Start from the file module QN just like cbm_run_kotlin_lsp(). A source + * package_header replaces this during kotlin_lsp_process_file(); default- + * package files retain it so emitted caller_qn values still match the + * textual extractor's .. identity. */ + kotlin_lsp_init(&ctx, arena, source, source_len, ®, module_qn ? module_qn : "", + module_qn ? module_qn : "", project_name, /*rel_path=*/NULL, out); /* Apply caller-supplied imports (resolved IMPORTS edges). */ for (int i = 0; i < import_count; i++) { diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 8b6cc4528..2b95ddd4b 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -31,6 +31,7 @@ #include "yyjson/yyjson.h" #include "foundation/compat_fs.h" +#include #include #include #include @@ -42,6 +43,11 @@ enum { PXC_ITOA_BUF = 16, }; +/* Hash-table keys reject empty strings, but Java/Kotlin's default package is a + * real shared namespace: sibling files can reference its declarations without + * imports. Use an internal key that cannot be a legal JVM package name. */ +static const char PXC_DEFAULT_JVM_NAMESPACE[] = ""; + /* Format an int into a thread-local rotating buffer for log key=value emission. * Mirrors the itoa_log helper in pass_calls.c — kept local so passes don't * grow a foundation-wide formatting API just for log output. */ @@ -467,14 +473,27 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca CBMArena keys; cbm_arena_init(&keys); CBMHashTable *seen = cbm_ht_create((uint32_t)(dst_calls->count + src_out->count + 1)); + CBMHashTable *by_call = cbm_ht_create((uint32_t)(dst_calls->count + src_out->count + 1)); + + if (!seen || !by_call) { + cbm_ht_free(seen); + cbm_ht_free(by_call); + cbm_arena_destroy(&keys); + return; + } for (int i = 0; i < dst_calls->count; i++) { - const CBMResolvedCall *rc = &dst_calls->items[i]; + CBMResolvedCall *rc = &dst_calls->items[i]; if (rc->caller_qn && rc->callee_qn) { char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", rc->caller_qn, rc->callee_qn); if (k) { cbm_ht_set(seen, k, (void *)1); } + char *call_key = cbm_arena_sprintf(&keys, "%s\x1f%s", rc->caller_qn, + pxc_last_component(rc->callee_qn)); + if (call_key && !cbm_ht_has(by_call, call_key)) { + cbm_ht_set(by_call, call_key, (void *)(uintptr_t)(i + 1)); + } } } @@ -482,6 +501,25 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca const CBMResolvedCall *src = &src_out->items[j]; if (!src->caller_qn || !src->callee_qn) continue; + char *call_key = cbm_arena_sprintf(&keys, "%s\x1f%s", src->caller_qn, + pxc_last_component(src->callee_qn)); + uintptr_t encoded_index = call_key ? (uintptr_t)cbm_ht_get(by_call, call_key) : 0; + if (encoded_index > 0) { + CBMResolvedCall *existing = &dst_calls->items[encoded_index - 1]; + /* The source array is produced by the cross-file pass and has a + * project-wide registry. On equal confidence it must replace the + * earlier per-file guess (for example stdlib maxOf versus a local + * maxOf); strictly weaker cross results never displace it. */ + if (src->confidence >= existing->confidence && + strcmp(src->callee_qn, existing->callee_qn) != 0) { + existing->callee_qn = cbm_arena_strdup(dst_arena, src->callee_qn); + existing->strategy = + src->strategy ? cbm_arena_strdup(dst_arena, src->strategy) : NULL; + existing->confidence = src->confidence; + existing->reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL; + } + continue; + } char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", src->caller_qn, src->callee_qn); if (k && cbm_ht_has(seen, k)) continue; @@ -496,9 +534,13 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca dst.confidence = src->confidence; dst.reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL; cbm_resolvedcall_push(dst_calls, dst_arena, dst); + if (call_key) { + cbm_ht_set(by_call, call_key, (void *)(uintptr_t)dst_calls->count); + } } cbm_ht_free(seen); + cbm_ht_free(by_call); cbm_arena_destroy(&keys); } @@ -1273,8 +1315,11 @@ CBMModuleDefIndex *cbm_pxc_build_module_def_index(CBMLSPDef *all_defs, int def_c for (int i = 0; i < def_count; i++) { pxc_module_entry_add_index(pxc_module_entry_get_or_create(ht, all_defs[i].def_module_qn), i); - pxc_module_entry_add_index( - pxc_module_entry_get_or_create(namespace_ht, all_defs[i].namespace_name), i); + const char *namespace_key = all_defs[i].namespace_name; + if (pxc_is_jvm_lang(all_defs[i].lang) && (!namespace_key || !namespace_key[0])) { + namespace_key = PXC_DEFAULT_JVM_NAMESPACE; + } + pxc_module_entry_add_index(pxc_module_entry_get_or_create(namespace_ht, namespace_key), i); } CBMModuleDefIndex *idx = (CBMModuleDefIndex *)calloc(1, sizeof(*idx)); @@ -1327,10 +1372,12 @@ CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef for (int i = 0; i < imp_count; i++) { pxc_mark_module_defs(idx, selected, all_defs, caller_lang, imp_qns[i], &total); } - if (pxc_is_jvm_lang(caller_lang) && caller_namespace && caller_namespace[0] && - idx->namespace_ht) { + if (pxc_is_jvm_lang(caller_lang) && idx->namespace_ht) { + const char *namespace_key = + (caller_namespace && caller_namespace[0]) ? caller_namespace + : PXC_DEFAULT_JVM_NAMESPACE; pxc_module_entry_t *e = - (pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, caller_namespace); + (pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, namespace_key); total += pxc_mark_entry_defs(selected, e, all_defs, caller_lang); }