fix(kotlin): resolve default-package project calls

Index Java and Kotlin definitions with an internal <jvm-default-package> 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 <project>.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 <project>.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 <project>.Main.run to <project>.Algo.maxOf. git diff --check passed.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-14 03:07:48 -04:00
parent b36e790b14
commit 09c0762938
2 changed files with 76 additions and 24 deletions
+23 -18
View File
@@ -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
* ("<project>.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, &reg, "", 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 <project>.<File>.<function> identity. */
kotlin_lsp_init(&ctx, arena, source, source_len, &reg, 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++) {
+53 -6
View File
@@ -31,6 +31,7 @@
#include "yyjson/yyjson.h"
#include "foundation/compat_fs.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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[] = "<jvm-default-package>";
/* 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);
}