Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

4366 lines
176 KiB
C

/*
* kotlin_lsp.c — Kotlin Light Semantic Pass implementation.
*
* See kotlin_lsp.h for the architectural overview. This file walks the
* tree-sitter Kotlin AST, populates a per-file CBMTypeRegistry, builds
* lexical scopes, infers expression types, and emits CBMResolvedCall
* entries when a call's target FQN can be determined statically.
*
* Reverse-engineered from the official Kotlin language specification
* (kotlinlang.org/spec/) and the reference fwcd/kotlin-language-server
* implementation. No JVM, no PSI, no BindingContext — pure C, walking
* tree-sitter syntax with hand-rolled name resolution.
*
* Confidence model:
* - 0.95 — direct constructor call, top-level fun, exact registry hit
* - 0.90 — method dispatch through known receiver type
* - 0.85 — extension function dispatch
* - 0.80 — companion / object-singleton call
* - 0.75 — super-call (with class-hierarchy lookup)
* - 0.70 — `this.x()` where `this_type` is known
* - 0.65 — partially resolved navigation (last segment)
* - 0.60 — minimum floor for the LSP override path; below = drop
*
* Strategies emitted:
* - "lsp_kt_constructor" — Foo() / Foo(args)
* - "lsp_kt_method" — receiver.method() with known receiver type
* - "lsp_kt_extension" — extension function dispatch
* - "lsp_kt_static" — Foo.bar() where Foo is object/companion
* - "lsp_kt_top_level" — bare top-level fun call
* - "lsp_kt_super" — super.foo() / super<Type>.foo()
* - "lsp_kt_this" — this.foo() with resolved this-type
* - "lsp_kt_safe" — obj?.foo() with known obj type
* - "lsp_kt_lambda_it" — it.foo() inside scope-function lambda
* - "lsp_kt_import" — direct import target hit (no receiver)
*/
#include "kotlin_lsp.h"
#include "../helpers.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Minimal Kotlin universal builtins as real graph nodes (kt_builtins_inject_defs).
* Amalgamation-included (see lsp_all.c); mirror of py_builtins.c. */
#include "kotlin_builtins.c"
#define KT_EVAL_MAX_DEPTH 32
#define KT_IMPORT_INITIAL_CAP 16
#define KT_CONF_CONSTRUCTOR 0.95f
#define KT_CONF_METHOD 0.90f
#define KT_CONF_EXTENSION 0.85f
#define KT_CONF_STATIC 0.80f
#define KT_CONF_SUPER 0.75f
#define KT_CONF_THIS 0.70f
#define KT_CONF_PARTIAL 0.65f
#define KT_CONF_TOP_LEVEL 0.95f
#define KT_CONF_LAMBDA_IT 0.78f
#define KT_CONF_IMPORT 0.92f
/* ── forward declarations ─────────────────────────────────────────── */
static void kt_resolve_calls_in_node_inner(KotlinLSPContext *ctx, TSNode node);
/* Depth-guarded entry for the AST call-resolution walk. The walk recurses once
* per nesting level; a deeply-nested or cyclic file can overflow the native
* stack (SIGSEGV) and take down the whole index. Past the cap the subtree is
* skipped — its calls stay unresolved, which is graceful degradation, not a
* crash. The cap is CBM_LSP_MAX_WALK_DEPTH, env-overridable via the same name.
* The walk_depth-- runs after the inner returns, so early returns in the body
* never leak the counter. */
static void kt_resolve_calls_in_node(KotlinLSPContext *ctx, TSNode node) {
if (ctx->walk_depth >= cbm_lsp_max_walk_depth())
return;
ctx->walk_depth++;
kt_resolve_calls_in_node_inner(ctx, node);
ctx->walk_depth--;
}
static void kt_process_class_decl(KotlinLSPContext *ctx, TSNode node);
static void kt_process_object_decl(KotlinLSPContext *ctx, TSNode node, bool is_companion,
const char *outer_class_qn);
static void kt_process_function_decl(KotlinLSPContext *ctx, TSNode node);
static void kt_process_property_decl(KotlinLSPContext *ctx, TSNode node);
static void kt_register_class_members(KotlinLSPContext *ctx, const char *class_qn, TSNode body,
bool is_object);
static const CBMType *kt_eval_call_expression_type(KotlinLSPContext *ctx, TSNode node);
static const CBMType *kt_eval_navigation_expression_type(KotlinLSPContext *ctx, TSNode node);
static const CBMType *kt_eval_user_type(KotlinLSPContext *ctx, TSNode node);
static void kt_process_block_stmts(KotlinLSPContext *ctx, TSNode block);
static void kt_bind_function_params(KotlinLSPContext *ctx, TSNode func_node);
static const char *kt_type_qn_of(const CBMType *t);
static char *kt_join_dot(CBMArena *a, const char *prefix, const char *name);
static const char *kt_short(const char *qn);
static char *kt_node_text(KotlinLSPContext *ctx, TSNode node);
static TSNode kt_child_kind(TSNode parent, const char *kind);
static TSNode kt_child_kind_named(TSNode parent, const char *kind);
static TSNode kt_field_named(TSNode parent, const char *field);
static bool kt_node_is(TSNode n, const char *kind);
static bool kt_node_kind_in(TSNode n, const char *const *kinds);
static const char *kt_resolve_in_default_imports(KotlinLSPContext *ctx, const char *name,
CBMKotlinUseKind kind);
static const CBMType *kt_try_smart_cast(KotlinLSPContext *ctx, TSNode call_or_nav);
/* ── helpers ──────────────────────────────────────────────────────── */
static char *kt_node_text(KotlinLSPContext *ctx, TSNode node) {
if (ts_node_is_null(node)) {
return NULL;
}
return cbm_node_text(ctx->arena, node, ctx->source);
}
/* Cross-grammar name-child resolver. Newer tree-sitter-kotlin dropped the
* `name` field and names value decls (functions, vars, params) with
* `simple_identifier` and type decls (classes, objects) with `type_identifier`;
* older grammars and import/package paths use `identifier`. kt_child_kind_named
* searches DIRECT named children only, so a class's nested constructor-param
* simple_identifiers are never mistaken for the class name. */
static TSNode kt_name_child(TSNode node) {
static const char *const kinds[] = {"simple_identifier", "type_identifier", "identifier"};
for (int i = 0; i < 3; i++) {
TSNode n = kt_child_kind_named(node, kinds[i]);
if (!ts_node_is_null(n)) {
return n;
}
}
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
return null_node;
}
static bool kt_node_is(TSNode n, const char *kind) {
if (ts_node_is_null(n)) {
return false;
}
return strcmp(ts_node_type(n), kind) == 0;
}
static bool kt_node_kind_in(TSNode n, const char *const *kinds) {
if (ts_node_is_null(n)) {
return false;
}
const char *k = ts_node_type(n);
for (int i = 0; kinds[i]; i++) {
if (strcmp(k, kinds[i]) == 0) {
return true;
}
}
return false;
}
static TSNode kt_child_kind(TSNode parent, const char *kind) {
if (ts_node_is_null(parent)) {
return parent;
}
uint32_t nc = ts_node_child_count(parent);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_child(parent, i);
if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) {
return c;
}
}
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
return null_node;
}
static TSNode kt_child_kind_named(TSNode parent, const char *kind) {
if (ts_node_is_null(parent)) {
return parent;
}
uint32_t nc = ts_node_named_child_count(parent);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(parent, i);
if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) {
return c;
}
}
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
return null_node;
}
static TSNode kt_field_named(TSNode parent, const char *field) {
if (ts_node_is_null(parent)) {
return parent;
}
return ts_node_child_by_field_name(parent, field, (uint32_t)strlen(field));
}
/* Find the first descendant matching kind, depth-first, capped at max_depth. */
static TSNode kt_find_descendant_kind(TSNode node, const char *kind, int max_depth) {
if (ts_node_is_null(node) || max_depth <= 0) {
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
return null_node;
}
if (strcmp(ts_node_type(node), kind) == 0) {
return node;
}
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode found = kt_find_descendant_kind(ts_node_child(node, i), kind, max_depth - 1);
if (!ts_node_is_null(found)) {
return found;
}
}
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
return null_node;
}
/* Return last segment after final '.'. */
static const char *kt_short(const char *qn) {
if (!qn) {
return NULL;
}
const char *last = qn;
for (const char *p = qn; *p; p++) {
if (*p == '.') {
last = p + 1;
}
}
return last;
}
static char *kt_join_dot(CBMArena *a, const char *prefix, const char *name) {
if (!a || !name) {
return NULL;
}
if (!prefix || !*prefix) {
return cbm_arena_strdup(a, name);
}
size_t pl = strlen(prefix);
size_t nl = strlen(name);
char *out = (char *)cbm_arena_alloc(a, pl + 1 + nl + 1);
if (!out) {
return NULL;
}
memcpy(out, prefix, pl);
out[pl] = '.';
memcpy(out + pl + 1, name, nl);
out[pl + 1 + nl] = '\0';
return out;
}
static const char *kt_type_qn_of(const CBMType *t) {
if (!t) {
return NULL;
}
switch (t->kind) {
case CBM_TYPE_NAMED:
return t->data.named.qualified_name;
case CBM_TYPE_BUILTIN:
return t->data.builtin.name;
case CBM_TYPE_TEMPLATE:
return t->data.template_type.template_name;
case CBM_TYPE_ALIAS:
if (t->data.alias.underlying) {
return kt_type_qn_of(t->data.alias.underlying);
}
return t->data.alias.alias_qn;
case CBM_TYPE_POINTER:
return kt_type_qn_of(t->data.pointer.elem);
case CBM_TYPE_REFERENCE:
return kt_type_qn_of(t->data.reference.elem);
default:
return NULL;
}
}
/* Strip a single trailing '?' (nullable type). The tree-sitter grammar
* exposes `nullable_type`; this utility flattens it. */
static const CBMType *kt_unwrap_nullable(const CBMType *t) {
/* For our purposes, T? and T are the same — we don't track nullability
* at the type level. Smart-cast handlers below treat them as the same.
*/
return t;
}
/* Append a CBMResolvedCall to the output array. */
static void kt_emit_resolved(KotlinLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
if (ctx->debug) {
fprintf(stderr, "[kotlin_lsp] EMIT %s -> %s [%s %.2f] (resolved_calls=%p enc=%s)\n",
ctx->enclosing_func_qn ? ctx->enclosing_func_qn : "(null)",
callee_qn ? callee_qn : "(null)", strategy ? strategy : "(null)",
(double)confidence, (void *)ctx->resolved_calls,
ctx->enclosing_func_qn ? ctx->enclosing_func_qn : "(null)");
}
if (!ctx->resolved_calls || !ctx->enclosing_func_qn || !callee_qn) {
return;
}
if (confidence < 0.60f) {
return;
}
CBMResolvedCallArray *arr = ctx->resolved_calls;
if (arr->count >= arr->cap) {
int new_cap = arr->cap == 0 ? 16 : arr->cap * 2;
CBMResolvedCall *new_items = (CBMResolvedCall *)cbm_arena_alloc(
ctx->arena, (size_t)new_cap * sizeof(CBMResolvedCall));
if (!new_items) {
return;
}
if (arr->items && arr->count > 0) {
memcpy(new_items, arr->items, (size_t)arr->count * sizeof(CBMResolvedCall));
}
arr->items = new_items;
arr->cap = new_cap;
}
CBMResolvedCall *rc = &arr->items[arr->count];
memset(rc, 0, sizeof(CBMResolvedCall));
rc->caller_qn = ctx->enclosing_func_qn;
rc->callee_qn = cbm_arena_strdup(ctx->arena, callee_qn);
rc->strategy = strategy;
rc->confidence = confidence;
arr->count++;
}
/* Detect a function_declaration's extension receiver type. The
* tree-sitter Kotlin grammar inlines the rule `_receiver_type` (a hidden
* rule starting with '_'), so the receiver doesn't appear as a named
* child of that name — instead, a `user_type` (or `nullable_type` /
* `parenthesized_type`) named child appears in source order BEFORE the
* `identifier` (function name).
*
* Returns the inner type node and writes its raw text to `recv_text_out`
* (arena-allocated). Returns null-node if the function is not an
* extension function. */
static TSNode kt_find_extension_receiver(KotlinLSPContext *ctx, TSNode func_node, TSNode name_node,
char **recv_text_out) {
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
if (recv_text_out) {
*recv_text_out = NULL;
}
if (ts_node_is_null(func_node) || ts_node_is_null(name_node)) {
return null_node;
}
/* Try field first (some grammar variants expose this). */
TSNode by_field = kt_field_named(func_node, "receiver");
if (!ts_node_is_null(by_field)) {
if (recv_text_out) {
*recv_text_out = kt_node_text(ctx, by_field);
}
return by_field;
}
/* Walk named children for a type-shaped node before the name. */
uint32_t name_start = ts_node_start_byte(name_node);
uint32_t nc = ts_node_named_child_count(func_node);
static const char *type_kinds[] = {"user_type", "nullable_type", "non_nullable_type",
"parenthesized_type", "type", NULL};
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(func_node, i);
if (ts_node_is_null(c)) {
continue;
}
if (ts_node_start_byte(c) >= name_start) {
break;
}
if (kt_node_kind_in(c, type_kinds)) {
if (recv_text_out) {
*recv_text_out = kt_node_text(ctx, c);
}
return c;
}
}
return null_node;
}
/* Extract the return type node from a function_declaration. The grammar
* exposes it via a `type` field or as a `type`/`user_type`/`nullable_type`
* named child appearing AFTER the `function_value_parameters` and
* BEFORE the `function_body`. */
static TSNode kt_find_return_type(TSNode func_node) {
TSNode null_node;
memset(&null_node, 0, sizeof(null_node));
if (ts_node_is_null(func_node)) {
return null_node;
}
TSNode by_field = kt_field_named(func_node, "type");
if (!ts_node_is_null(by_field)) {
return by_field;
}
/* Walk children for a type appearing after the parameters. */
TSNode params = kt_child_kind(func_node, "function_value_parameters");
uint32_t after_params = ts_node_is_null(params) ? 0 : ts_node_end_byte(params);
uint32_t nc = ts_node_named_child_count(func_node);
static const char *type_kinds[] = {"user_type",
"nullable_type",
"non_nullable_type",
"parenthesized_type",
"type",
"function_type",
NULL};
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(func_node, i);
if (ts_node_is_null(c)) {
continue;
}
if (ts_node_start_byte(c) < after_params) {
continue;
}
const char *k = ts_node_type(c);
if (strcmp(k, "function_body") == 0 || strcmp(k, "block") == 0) {
break;
}
if (kt_node_kind_in(c, type_kinds)) {
return c;
}
}
return null_node;
}
/* Build a CBM_TYPE_FUNC signature carrying just the return type, so that
* cbm_registry_lookup_method's caller can read rf->signature->return_types
* to determine the chained receiver. param_names/param_types are left
* empty — we only care about the return type for chain tracking. */
static const CBMType *kt_build_func_sig_with_return(KotlinLSPContext *ctx,
const CBMType *return_type) {
if (!return_type || cbm_type_is_unknown(return_type)) {
return NULL;
}
const char **empty_pn = (const char **)cbm_arena_alloc(ctx->arena, sizeof(const char *));
if (!empty_pn) {
return NULL;
}
empty_pn[0] = NULL;
const CBMType **empty_pt =
(const CBMType **)cbm_arena_alloc(ctx->arena, sizeof(const CBMType *));
if (!empty_pt) {
return NULL;
}
empty_pt[0] = NULL;
const CBMType **rt = (const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(const CBMType *));
if (!rt) {
return NULL;
}
rt[0] = return_type;
rt[1] = NULL;
return cbm_type_func(ctx->arena, empty_pn, empty_pt, rt);
}
/* ── public API ───────────────────────────────────────────────────── */
void kotlin_lsp_init(KotlinLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *package_qn, const char *module_qn,
const char *project_name, const char *rel_path, CBMResolvedCallArray *out) {
memset(ctx, 0, sizeof(KotlinLSPContext));
ctx->arena = arena;
ctx->source = source;
ctx->source_len = source_len;
ctx->registry = registry;
ctx->package_qn = package_qn ? cbm_arena_strdup(arena, package_qn) : "";
ctx->module_qn = module_qn ? cbm_arena_strdup(arena, module_qn) : "";
ctx->project_name = project_name ? cbm_arena_strdup(arena, project_name) : "";
ctx->rel_path = rel_path ? cbm_arena_strdup(arena, rel_path) : "";
ctx->resolved_calls = out;
ctx->import_cap = KT_IMPORT_INITIAL_CAP;
ctx->import_locals =
(const char **)cbm_arena_alloc(arena, sizeof(const char *) * (size_t)ctx->import_cap);
ctx->import_targets =
(const char **)cbm_arena_alloc(arena, sizeof(const char *) * (size_t)ctx->import_cap);
ctx->import_kinds = (CBMKotlinUseKind *)cbm_arena_alloc(arena, sizeof(CBMKotlinUseKind) *
(size_t)ctx->import_cap);
ctx->import_count = 0;
ctx->debug = (getenv("CBM_LSP_DEBUG") != NULL);
/* Compute the JVM file class name. The Kotlin convention is that
* top-level functions/properties live in a synthetic class named
* "<FilenameWithoutExt>Kt". For internal references we don't usually
* need this, but we expose it for completeness. */
if (rel_path && *rel_path) {
const char *base = rel_path;
for (const char *p = rel_path; *p; p++) {
if (*p == '/' || *p == '\\') {
base = p + 1;
}
}
const char *dot = strrchr(base, '.');
size_t name_len = dot ? (size_t)(dot - base) : strlen(base);
char *fn = (char *)cbm_arena_alloc(arena, name_len + 3);
if (fn) {
memcpy(fn, base, name_len);
/* Capitalize first letter (Kotlin file class convention is
* to keep filename casing, but for our purposes the literal
* filename suffices). */
if (name_len > 0 && fn[0] >= 'a' && fn[0] <= 'z') {
fn[0] = (char)(fn[0] - 'a' + 'A');
}
fn[name_len] = 'K';
fn[name_len + 1] = 't';
fn[name_len + 2] = '\0';
ctx->file_class_qn = kt_join_dot(arena, ctx->package_qn, fn);
}
}
/* Push the file-level scope. */
ctx->current_scope = cbm_scope_push(arena, NULL);
}
void kotlin_lsp_add_import(KotlinLSPContext *ctx, const char *local_name, const char *target_qn,
CBMKotlinUseKind kind) {
if (!ctx || !target_qn) {
return;
}
if (ctx->import_count >= ctx->import_cap) {
int new_cap = ctx->import_cap * 2;
const char **new_locals =
(const char **)cbm_arena_alloc(ctx->arena, sizeof(const char *) * (size_t)new_cap);
const char **new_targets =
(const char **)cbm_arena_alloc(ctx->arena, sizeof(const char *) * (size_t)new_cap);
CBMKotlinUseKind *new_kinds = (CBMKotlinUseKind *)cbm_arena_alloc(
ctx->arena, sizeof(CBMKotlinUseKind) * (size_t)new_cap);
if (!new_locals || !new_targets || !new_kinds) {
return;
}
memcpy(new_locals, ctx->import_locals, sizeof(const char *) * (size_t)ctx->import_count);
memcpy(new_targets, ctx->import_targets, sizeof(const char *) * (size_t)ctx->import_count);
memcpy(new_kinds, ctx->import_kinds, sizeof(CBMKotlinUseKind) * (size_t)ctx->import_count);
ctx->import_locals = new_locals;
ctx->import_targets = new_targets;
ctx->import_kinds = new_kinds;
ctx->import_cap = new_cap;
}
ctx->import_locals[ctx->import_count] =
local_name ? cbm_arena_strdup(ctx->arena, local_name) : NULL;
ctx->import_targets[ctx->import_count] = cbm_arena_strdup(ctx->arena, target_qn);
ctx->import_kinds[ctx->import_count] = kind;
ctx->import_count++;
}
const char *kotlin_resolve_class_name(KotlinLSPContext *ctx, const char *name) {
if (!ctx || !name || !*name) {
return NULL;
}
/* Already qualified? */
if (strchr(name, '.')) {
/* Prefix with project? Best-effort heuristic: leave as-is, the
* pipeline will retry with project prefix on miss. */
return cbm_arena_strdup(ctx->arena, name);
}
/* Same-package lookup: <package>.<name> */
if (ctx->package_qn && *ctx->package_qn) {
char *cand = kt_join_dot(ctx->arena, ctx->package_qn, name);
if (ctx->registry && cbm_registry_lookup_type(ctx->registry, cand)) {
return cand;
}
/* Project-qualified candidate — keep alongside as fallback */
}
/* Bare-name lookup: when the file has no `package` declaration, all
* user-defined types are registered under their unqualified name.
* Check this BEFORE default imports so a user-defined `StringBuilder`
* or `Logger` shadows the stdlib one with the same short name —
* matching how the official compiler resolves names. */
if (ctx->registry && cbm_registry_lookup_type(ctx->registry, name)) {
return cbm_arena_strdup(ctx->arena, name);
}
/* Explicit imports */
for (int i = 0; i < ctx->import_count; i++) {
const char *local = ctx->import_locals[i];
if (local && strcmp(local, name) == 0 &&
(ctx->import_kinds[i] == CBM_KT_USE_TYPE ||
ctx->import_kinds[i] == CBM_KT_USE_UNKNOWN)) {
return ctx->import_targets[i];
}
}
/* Wildcard imports + default imports */
for (int i = 0; i < ctx->import_count; i++) {
if (ctx->import_kinds[i] == CBM_KT_USE_WILDCARD && ctx->import_targets[i]) {
char *cand = kt_join_dot(ctx->arena, ctx->import_targets[i], name);
if (ctx->registry && cbm_registry_lookup_type(ctx->registry, cand)) {
return cand;
}
}
}
int n = 0;
const char *const *defs = cbm_kotlin_default_import_packages(&n);
for (int i = 0; i < n; i++) {
char *cand = kt_join_dot(ctx->arena, defs[i], name);
if (ctx->registry && cbm_registry_lookup_type(ctx->registry, cand)) {
return cand;
}
}
/* Same-package fallback — even if not in registry, useful for cross-file
* lookups via the pipeline's <project>.<qn> retry. */
if (ctx->package_qn && *ctx->package_qn) {
return kt_join_dot(ctx->arena, ctx->package_qn, name);
}
return cbm_arena_strdup(ctx->arena, name);
}
const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name) {
if (!ctx || !name || !*name) {
return NULL;
}
if (strchr(name, '.')) {
return cbm_arena_strdup(ctx->arena, name);
}
/* Explicit function import */
for (int i = 0; i < ctx->import_count; i++) {
const char *local = ctx->import_locals[i];
if (local && strcmp(local, name) == 0 &&
(ctx->import_kinds[i] == CBM_KT_USE_FUNCTION ||
ctx->import_kinds[i] == CBM_KT_USE_UNKNOWN ||
ctx->import_kinds[i] == CBM_KT_USE_TYPE)) {
return ctx->import_targets[i];
}
}
/* Same-package top-level fun */
if (ctx->package_qn && *ctx->package_qn) {
char *cand = kt_join_dot(ctx->arena, ctx->package_qn, name);
if (ctx->registry && cbm_registry_lookup_func(ctx->registry, cand)) {
return cand;
}
}
/* Bare-name lookup for files without a package declaration. */
if (ctx->registry && cbm_registry_lookup_func(ctx->registry, name)) {
return cbm_arena_strdup(ctx->arena, name);
}
/* Wildcard imports */
for (int i = 0; i < ctx->import_count; i++) {
if (ctx->import_kinds[i] == CBM_KT_USE_WILDCARD && ctx->import_targets[i]) {
char *cand = kt_join_dot(ctx->arena, ctx->import_targets[i], name);
if (ctx->registry && cbm_registry_lookup_func(ctx->registry, cand)) {
return cand;
}
}
}
/* 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`
* 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. */
if (ctx->registry && ctx->registry->funcs) {
const char *only_qn = NULL;
int matches = 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) {
continue;
}
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
continue;
}
if (strcmp(f->short_name, name) == 0) {
only_qn = f->qualified_name;
matches++;
}
}
if (matches == 1 && only_qn) {
return cbm_arena_strdup(ctx->arena, only_qn);
}
}
return NULL;
}
static const char *kt_resolve_in_default_imports(KotlinLSPContext *ctx, const char *name,
CBMKotlinUseKind kind) {
int n = 0;
const char *const *defs = cbm_kotlin_default_import_packages(&n);
for (int i = 0; i < n; i++) {
char *cand = kt_join_dot(ctx->arena, defs[i], name);
if (!ctx->registry) {
continue;
}
if (kind == CBM_KT_USE_FUNCTION || kind == CBM_KT_USE_UNKNOWN) {
if (cbm_registry_lookup_func(ctx->registry, cand)) {
return cand;
}
}
if (kind == CBM_KT_USE_TYPE || kind == CBM_KT_USE_UNKNOWN) {
if (cbm_registry_lookup_type(ctx->registry, cand)) {
return cand;
}
}
}
return NULL;
}
/* Synthesize a CBMRegisteredFunc on the fly for stdlib types where we
* only have a method_names array (no full func registration). The
* lifetime of the synthesized struct is the LSP context's arena. */
static const CBMRegisteredFunc *kt_synth_method(KotlinLSPContext *ctx, const char *class_qn,
const char *method_name) {
CBMRegisteredFunc *rf =
(CBMRegisteredFunc *)cbm_arena_alloc(ctx->arena, sizeof(CBMRegisteredFunc));
if (!rf) {
return NULL;
}
memset(rf, 0, sizeof(CBMRegisteredFunc));
rf->qualified_name = kt_join_dot(ctx->arena, class_qn, method_name);
rf->receiver_type = cbm_arena_strdup(ctx->arena, class_qn);
rf->short_name = cbm_arena_strdup(ctx->arena, method_name);
rf->min_params = 0;
return rf;
}
/* Heuristic: stdlib methods that return the same receiver type
* (fluent style) — used for return-type tracking across chains. */
static bool kt_method_returns_self(const char *class_qn, const char *method) {
if (!class_qn || !method) {
return false;
}
if (strcmp(class_qn, "kotlin.String") == 0 || strcmp(class_qn, "java.lang.String") == 0 ||
strcmp(class_qn, "kotlin.CharSequence") == 0 ||
strcmp(class_qn, "kotlin.text.StringBuilder") == 0) {
const char *self_returning[] = {
"trim", "trimStart",
"trimEnd", "trimIndent",
"trimMargin", "uppercase",
"lowercase", "replace",
"padStart", "padEnd",
"repeat", "removePrefix",
"removeSuffix", "removeSurrounding",
"replaceFirst", "reversed",
"substring", "intern",
"format", "plus",
"subSequence", NULL,
};
for (int i = 0; self_returning[i]; i++) {
if (strcmp(method, self_returning[i]) == 0) {
return true;
}
}
}
if (strstr(class_qn, ".List") || strstr(class_qn, ".MutableList") ||
strstr(class_qn, ".Iterable") || strstr(class_qn, ".Collection")) {
const char *self_returning[] = {
"filter", "filterNot", "filterNotNull", "filterIndexed",
"map", "mapNotNull", "mapIndexed", "flatMap",
"sorted", "sortedBy", "sortedDescending", "sortedByDescending",
"sortedWith", "reversed", "distinct", "distinctBy",
"take", "takeLast", "takeWhile", "takeLastWhile",
"drop", "dropLast", "dropWhile", "dropLastWhile",
"shuffled", "asReversed", "toList", "toMutableList",
"ifEmpty", NULL,
};
for (int i = 0; self_returning[i]; i++) {
if (strcmp(method, self_returning[i]) == 0) {
return true;
}
}
}
/* Builder pattern — methods named `add`, `append`, `with` typically
* return self. Only triggered when no other info is available. */
if (strcmp(method, "add") == 0 || strcmp(method, "append") == 0 ||
strcmp(method, "with") == 0 || strcmp(method, "set") == 0) {
return true;
}
return false;
}
/* Returns the return type of a known stdlib method when we can determine
* it from heuristic rules (self-returning, common String/Int/Boolean
* results). Best-effort — returns UNKNOWN when we don't know. */
static const CBMType *kt_stdlib_method_return_type(KotlinLSPContext *ctx, const char *class_qn,
const char *method) {
if (!class_qn || !method) {
return cbm_type_unknown();
}
if (kt_method_returns_self(class_qn, method)) {
return cbm_type_named(ctx->arena, class_qn);
}
if (strcmp(method, "toString") == 0 || strcmp(method, "toRegex") == 0 ||
strcmp(method, "joinToString") == 0 || strcmp(method, "toLowerCase") == 0 ||
strcmp(method, "toUpperCase") == 0) {
return cbm_type_named(ctx->arena, "kotlin.String");
}
if (strcmp(method, "size") == 0 || strcmp(method, "length") == 0 ||
strcmp(method, "count") == 0 || strcmp(method, "indexOf") == 0 ||
strcmp(method, "lastIndexOf") == 0 || strcmp(method, "compareTo") == 0 ||
strcmp(method, "hashCode") == 0 || strcmp(method, "code") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Int");
}
if (strcmp(method, "isEmpty") == 0 || strcmp(method, "isNotEmpty") == 0 ||
strcmp(method, "isBlank") == 0 || strcmp(method, "isNotBlank") == 0 ||
strcmp(method, "contains") == 0 || strcmp(method, "containsAll") == 0 ||
strcmp(method, "containsKey") == 0 || strcmp(method, "containsValue") == 0 ||
strcmp(method, "startsWith") == 0 || strcmp(method, "endsWith") == 0 ||
strcmp(method, "equals") == 0 || strcmp(method, "any") == 0 || strcmp(method, "all") == 0 ||
strcmp(method, "none") == 0 || strcmp(method, "matches") == 0 ||
strcmp(method, "isDigit") == 0 || strcmp(method, "isLetter") == 0 ||
strcmp(method, "isWhitespace") == 0 || strcmp(method, "isFile") == 0 ||
strcmp(method, "isDirectory") == 0 || strcmp(method, "exists") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Boolean");
}
if (strcmp(method, "sum") == 0 || strcmp(method, "sumOf") == 0 ||
strcmp(method, "sumBy") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Int");
}
return cbm_type_unknown();
}
/* Iterative method lookup with a single depth bound on the combined
* alias-then-super-chain walk. We never recurse — a class's alias chain
* is followed first (capped at 16 hops to break self-referential cycles
* that arise when a typealias resolves to a name that the registry maps
* back to the same type), then the super chain is walked breadth-first
* with a small visited set to break diamond inheritance. */
const CBMRegisteredFunc *kotlin_lookup_method(KotlinLSPContext *ctx, const char *class_qn,
const char *method_name) {
if (!ctx || !class_qn || !method_name || !ctx->registry) {
return NULL;
}
/* Walk the alias chain iteratively, then the super chain, all using
* a single visited-set so we cannot loop. */
enum { VISIT_CAP = 32 };
const char *visited[VISIT_CAP];
int visited_n = 0;
const char *queue[VISIT_CAP];
int qhead = 0;
int qtail = 0;
queue[qtail++] = class_qn;
while (qhead < qtail) {
const char *cur_qn = queue[qhead++];
if (!cur_qn) {
continue;
}
bool seen = false;
for (int v = 0; v < visited_n; v++) {
if (strcmp(visited[v], cur_qn) == 0) {
seen = true;
break;
}
}
if (seen) {
continue;
}
if (visited_n < VISIT_CAP) {
visited[visited_n++] = cur_qn;
}
/* Direct func registry hit (also follows alias chain internally
* via cbm_registry_lookup_method_aliased). */
const CBMRegisteredFunc *rf =
cbm_registry_lookup_method_aliased(ctx->registry, cur_qn, method_name);
if (rf) {
return rf;
}
/* method_names fallback */
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, cur_qn);
if (rt && rt->method_names) {
for (int i = 0; rt->method_names[i]; i++) {
if (strcmp(rt->method_names[i], method_name) == 0) {
return kt_synth_method(ctx, cur_qn, method_name);
}
}
}
if (!rt) {
continue;
}
/* Enqueue alias target */
if (rt->alias_of && qtail < VISIT_CAP) {
queue[qtail++] = rt->alias_of;
}
/* Enqueue super-chain */
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i] && qtail < VISIT_CAP; i++) {
queue[qtail++] = rt->embedded_types[i];
}
}
}
return NULL;
}
const CBMType *kotlin_lookup_property_type(KotlinLSPContext *ctx, const char *class_qn,
const char *prop_name) {
if (!ctx || !class_qn || !prop_name || !ctx->registry) {
return cbm_type_unknown();
}
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, class_qn);
if (!rt) {
return cbm_type_unknown();
}
if (rt->field_names && rt->field_types) {
for (int i = 0; rt->field_names[i]; i++) {
if (strcmp(rt->field_names[i], prop_name) == 0) {
return rt->field_types[i];
}
}
}
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
const CBMType *t = kotlin_lookup_property_type(ctx, rt->embedded_types[i], prop_name);
if (!cbm_type_is_unknown(t)) {
return t;
}
}
}
return cbm_type_unknown();
}
/* ── package and import parsing ───────────────────────────────────── */
static const char *kt_parse_package_header(KotlinLSPContext *ctx, TSNode header) {
if (ts_node_is_null(header)) {
return "";
}
/* package_header: 'package' identifier ('.' identifier)* ';'
* The grammar has either `qualified_identifier` or a sequence of
* identifiers; we extract the textual span between 'package' and
* the trailing semi/newline. */
TSNode qid = kt_child_kind_named(header, "qualified_identifier");
if (ts_node_is_null(qid)) {
qid = kt_name_child(header);
}
if (ts_node_is_null(qid)) {
return "";
}
char *txt = kt_node_text(ctx, qid);
if (!txt) {
return "";
}
/* Strip whitespace around dots */
char *out = (char *)cbm_arena_alloc(ctx->arena, strlen(txt) + 1);
if (!out) {
return "";
}
int oi = 0;
for (int i = 0; txt[i]; i++) {
if (txt[i] != ' ' && txt[i] != '\t' && txt[i] != '\n' && txt[i] != '\r') {
out[oi++] = txt[i];
}
}
out[oi] = '\0';
return out;
}
static void kt_parse_import_directive(KotlinLSPContext *ctx, TSNode imp) {
if (ts_node_is_null(imp)) {
return;
}
/* Tree-sitter shape:
* import 'import' qualified_identifier ('.*' | 'as' identifier)?
*/
TSNode qid = kt_child_kind_named(imp, "qualified_identifier");
if (ts_node_is_null(qid)) {
qid = kt_name_child(imp);
}
if (ts_node_is_null(qid)) {
return;
}
char *qid_text = kt_node_text(ctx, qid);
if (!qid_text) {
return;
}
/* Compress whitespace */
char *cleaned = (char *)cbm_arena_alloc(ctx->arena, strlen(qid_text) + 1);
int oi = 0;
for (int i = 0; qid_text[i]; i++) {
if (qid_text[i] != ' ' && qid_text[i] != '\t' && qid_text[i] != '\n' &&
qid_text[i] != '\r') {
cleaned[oi++] = qid_text[i];
}
}
cleaned[oi] = '\0';
/* Detect wildcard suffix `.*` in raw text */
char *full_text = kt_node_text(ctx, imp);
bool wildcard = (full_text && strstr(full_text, ".*"));
/* Detect `as <alias>` */
const char *alias = NULL;
if (full_text) {
const char *as_kw = strstr(full_text, " as ");
if (as_kw) {
const char *p = as_kw + 4;
while (*p == ' ' || *p == '\t') {
p++;
}
char *al = (char *)cbm_arena_alloc(ctx->arena, 128);
if (al) {
int ai = 0;
while (*p && (isalnum((unsigned char)*p) || *p == '_') && ai < 127) {
al[ai++] = *p++;
}
al[ai] = '\0';
if (ai > 0) {
alias = al;
}
}
}
}
if (wildcard) {
kotlin_lsp_add_import(ctx, NULL, cleaned, CBM_KT_USE_WILDCARD);
return;
}
/* Default: kind = UNKNOWN — we'll match it both ways at lookup time. */
const char *local = alias ? alias : kt_short(cleaned);
kotlin_lsp_add_import(ctx, local, cleaned, CBM_KT_USE_UNKNOWN);
}
/* ── definition collection ────────────────────────────────────────── */
/* Walk top-level declarations once to discover classes and top-level fns
* so that intra-file references can resolve regardless of ordering. */
static void kt_collect_top_level_decls(KotlinLSPContext *ctx, TSNode root) {
if (ts_node_is_null(root)) {
return;
}
uint32_t nc = ts_node_named_child_count(root);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(root, i);
const char *kind = ts_node_type(c);
/* The vendored Kotlin grammar can't parse `interface` at file
* scope (and a few other constructs), producing an ERROR node
* that wraps everything that followed. Recurse into ERROR so
* we can still register the class/function declarations the
* parser managed to recognize inside it. */
if (strcmp(kind, "ERROR") == 0) {
kt_collect_top_level_decls(ctx, c);
continue;
}
if (strcmp(kind, "class_declaration") == 0) {
kt_process_class_decl(ctx, c);
} else if (strcmp(kind, "object_declaration") == 0) {
kt_process_object_decl(ctx, c, false, NULL);
} else if (strcmp(kind, "function_declaration") == 0) {
kt_process_function_decl(ctx, c);
} else if (strcmp(kind, "property_declaration") == 0) {
kt_process_property_decl(ctx, c);
} else if (strcmp(kind, "type_alias") == 0) {
/* type_alias: 'typealias' name '=' type */
TSNode name = kt_field_named(c, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(c);
}
char *n = kt_node_text(ctx, name);
if (!n) {
continue;
}
CBMRegisteredType rt = {0};
rt.qualified_name = kt_join_dot(ctx->arena, ctx->package_qn, n);
rt.short_name = n;
/* Capture the underlying type and stamp alias_of so that
* cbm_registry_resolve_alias / lookup_method_aliased follow
* the chain to e.g. kotlin.Int's methods. */
TSNode rhs = kt_field_named(c, "type");
if (ts_node_is_null(rhs)) {
/* Find first type-shaped named child after the name */
static const char *type_kinds[] = {"user_type",
"nullable_type",
"non_nullable_type",
"parenthesized_type",
"type",
"function_type",
NULL};
uint32_t name_end = ts_node_end_byte(name);
uint32_t kc = ts_node_named_child_count(c);
for (uint32_t j = 0; j < kc; j++) {
TSNode tc = ts_node_named_child(c, j);
if (ts_node_is_null(tc)) {
continue;
}
if (ts_node_start_byte(tc) < name_end) {
continue;
}
if (kt_node_kind_in(tc, type_kinds)) {
rhs = tc;
break;
}
}
}
if (!ts_node_is_null(rhs)) {
const CBMType *underlying = kotlin_parse_type_node(ctx, rhs);
if (underlying && underlying->kind == CBM_TYPE_NAMED &&
underlying->data.named.qualified_name) {
rt.alias_of = underlying->data.named.qualified_name;
}
}
cbm_registry_add_type((CBMTypeRegistry *)ctx->registry, rt);
}
}
}
static const char *kt_qn_for_class_decl(KotlinLSPContext *ctx, TSNode class_node) {
TSNode name = kt_field_named(class_node, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(class_node);
}
if (ts_node_is_null(name)) {
return NULL;
}
char *short_name = kt_node_text(ctx, name);
if (!short_name) {
return NULL;
}
if (ctx->enclosing_class_qn) {
return kt_join_dot(ctx->arena, ctx->enclosing_class_qn, short_name);
}
return kt_join_dot(ctx->arena, ctx->package_qn, short_name);
}
static void kt_process_class_decl(KotlinLSPContext *ctx, TSNode node) {
const char *class_qn = kt_qn_for_class_decl(ctx, node);
if (!class_qn) {
return;
}
/* Register the class skeleton */
CBMRegisteredType rt = {0};
rt.qualified_name = class_qn;
rt.short_name = kt_short(class_qn);
/* Collect inheritance. Older grammars wrap specifiers in a
* `delegation_specifiers` node; newer tree-sitter-kotlin places
* `delegation_specifier` directly under the class declaration. */
TSNode delegation = kt_child_kind(node, "delegation_specifiers");
{
TSNode dcontainer = ts_node_is_null(delegation) ? node : delegation;
const char *parents[16];
int parent_count = 0;
uint32_t dnc = ts_node_named_child_count(dcontainer);
for (uint32_t di = 0; di < dnc && parent_count < 15; di++) {
TSNode dc = ts_node_named_child(dcontainer, di);
if (kt_node_is(dc, "delegation_specifier")) {
/* Find user_type or constructor_invocation */
TSNode ut = kt_find_descendant_kind(dc, "user_type", 4);
if (ts_node_is_null(ut)) {
ut = kt_find_descendant_kind(dc, "constructor_invocation", 4);
if (!ts_node_is_null(ut)) {
ut = kt_find_descendant_kind(ut, "user_type", 3);
}
}
if (!ts_node_is_null(ut)) {
char *name_text = kt_node_text(ctx, ut);
if (name_text) {
/* Strip generics */
char *lt = strchr(name_text, '<');
if (lt) {
*lt = '\0';
}
const char *resolved = kotlin_resolve_class_name(ctx, name_text);
if (resolved) {
parents[parent_count++] = resolved;
}
}
}
}
}
if (parent_count > 0) {
const char **embedded = (const char **)cbm_arena_alloc(
ctx->arena, sizeof(const char *) * (size_t)(parent_count + 1));
if (embedded) {
for (int p = 0; p < parent_count; p++) {
embedded[p] = parents[p];
}
embedded[parent_count] = NULL;
rt.embedded_types = embedded;
}
}
}
/* Determine if interface */
TSNode mods = kt_child_kind(node, "modifiers");
bool is_interface = false;
if (!ts_node_is_null(mods)) {
char *mod_text = kt_node_text(ctx, mods);
if (mod_text && strstr(mod_text, "interface") != NULL) {
is_interface = true;
}
}
/* Also possible: the class_declaration's first child is 'class' or
* 'interface' keyword text. */
{
char *full = kt_node_text(ctx, node);
if (full) {
/* crude check: starts-with "interface " (after modifiers). */
const char *p = full;
while (*p && (*p == ' ' || *p == '\t' || *p == '\n')) {
p++;
}
/* Skip annotations + modifiers tokens */
while (*p && *p != 'c' && *p != 'i') {
/* find class/interface keyword */
const char *cls = strstr(p, "class ");
const char *iface = strstr(p, "interface ");
if (cls && (!iface || cls < iface)) {
p = cls;
break;
}
if (iface) {
p = iface;
break;
}
p = "";
}
if (p && strncmp(p, "interface", 9) == 0) {
is_interface = true;
}
}
}
rt.is_interface = is_interface;
/* Collect class fields BEFORE registering the type. Source 1: primary
* constructor val/var parameters. Source 2: class-body val/var
* `property_declaration` nodes (e.g. `private val data = ...`). Both
* become accessible via `name`-based scope lookup inside any method. */
const char *fnames[64];
const CBMType *ftypes[64];
int field_count = 0;
TSNode pc = kt_child_kind(node, "primary_constructor");
if (!ts_node_is_null(pc)) {
/* Older grammars wrap params in a `class_parameters` node; newer
* tree-sitter-kotlin places `class_parameter` directly under the
* primary_constructor. */
TSNode params = kt_find_descendant_kind(pc, "class_parameters", 3);
TSNode pcontainer = ts_node_is_null(params) ? pc : params;
{
uint32_t pnc = ts_node_named_child_count(pcontainer);
for (uint32_t pi = 0; pi < pnc && field_count < 63; pi++) {
TSNode p = ts_node_named_child(pcontainer, pi);
if (!kt_node_is(p, "class_parameter")) {
continue;
}
char *p_text = kt_node_text(ctx, p);
if (!p_text) {
continue;
}
bool has_val_var = (strstr(p_text, "val ") || strstr(p_text, "var "));
if (!has_val_var) {
continue;
}
TSNode name_node = kt_field_named(p, "name");
if (ts_node_is_null(name_node)) {
name_node = kt_name_child(p);
}
if (ts_node_is_null(name_node)) {
continue;
}
char *fname = kt_node_text(ctx, name_node);
if (!fname) {
continue;
}
TSNode type_node = kt_field_named(p, "type");
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(p, "user_type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(p, "nullable_type");
}
const CBMType *ft = ts_node_is_null(type_node)
? cbm_type_unknown()
: kotlin_parse_type_node(ctx, type_node);
fnames[field_count] = fname;
ftypes[field_count] = ft;
field_count++;
}
}
}
/* Class-body property declarations. */
TSNode body_for_props = kt_child_kind(node, "class_body");
if (!ts_node_is_null(body_for_props)) {
uint32_t bnc = ts_node_named_child_count(body_for_props);
for (uint32_t i = 0; i < bnc && field_count < 63; i++) {
TSNode m = ts_node_named_child(body_for_props, i);
if (!kt_node_is(m, "property_declaration")) {
continue;
}
TSNode var = kt_child_kind(m, "variable_declaration");
if (ts_node_is_null(var)) {
continue;
}
TSNode id = kt_name_child(var);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(var, "simple_identifier");
}
if (ts_node_is_null(id)) {
continue;
}
char *pname = kt_node_text(ctx, id);
if (!pname) {
continue;
}
/* Determine type from annotation or initializer. */
TSNode type_node = kt_child_kind(var, "type");
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "user_type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "nullable_type");
}
const CBMType *pt = cbm_type_unknown();
if (!ts_node_is_null(type_node)) {
pt = kotlin_parse_type_node(ctx, type_node);
} else {
/* Try inferring from initializer expression. */
uint32_t mnc = ts_node_named_child_count(m);
for (uint32_t k = 0; k < mnc; k++) {
TSNode mc = ts_node_named_child(m, k);
const char *mk = ts_node_type(mc);
if (strcmp(mk, "variable_declaration") == 0 || strcmp(mk, "modifiers") == 0) {
continue;
}
pt = kotlin_eval_expr_type(ctx, mc);
if (!cbm_type_is_unknown(pt)) {
break;
}
}
}
fnames[field_count] = pname;
ftypes[field_count] = pt;
field_count++;
}
}
if (field_count > 0) {
const char **fn = (const char **)cbm_arena_alloc(ctx->arena, sizeof(const char *) *
(size_t)(field_count + 1));
const CBMType **ft = (const CBMType **)cbm_arena_alloc(
ctx->arena, sizeof(const CBMType *) * (size_t)(field_count + 1));
if (fn && ft) {
for (int i = 0; i < field_count; i++) {
fn[i] = fnames[i];
ft[i] = ftypes[i];
}
fn[field_count] = NULL;
ft[field_count] = NULL;
rt.field_names = fn;
rt.field_types = ft;
}
}
cbm_registry_add_type((CBMTypeRegistry *)ctx->registry, rt);
/* Recurse into body for nested types and members */
TSNode body = kt_child_kind(node, "class_body");
if (ts_node_is_null(body)) {
body = kt_child_kind(node, "enum_class_body");
}
if (!ts_node_is_null(body)) {
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = class_qn;
kt_register_class_members(ctx, class_qn, body, false);
ctx->enclosing_class_qn = prev_class;
}
}
static void kt_process_object_decl(KotlinLSPContext *ctx, TSNode node, bool is_companion,
const char *outer_class_qn) {
TSNode name = kt_field_named(node, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(node);
}
char *short_name = ts_node_is_null(name) ? NULL : kt_node_text(ctx, name);
const char *obj_qn = NULL;
if (is_companion) {
/* `companion object [Name]` — if no name given, use "Companion" */
const char *companion_name = (short_name && *short_name) ? short_name : "Companion";
obj_qn = kt_join_dot(ctx->arena, outer_class_qn ? outer_class_qn : ctx->enclosing_class_qn,
companion_name);
} else {
if (!short_name) {
return;
}
if (ctx->enclosing_class_qn) {
obj_qn = kt_join_dot(ctx->arena, ctx->enclosing_class_qn, short_name);
} else {
obj_qn = kt_join_dot(ctx->arena, ctx->package_qn, short_name);
}
}
CBMRegisteredType rt = {0};
rt.qualified_name = obj_qn;
rt.short_name = kt_short(obj_qn);
/* Inheritance for object (delegation_specifiers wrapper in older grammars,
* else delegation_specifier directly under the object declaration). */
TSNode delegation = kt_child_kind(node, "delegation_specifiers");
{
TSNode dcontainer = ts_node_is_null(delegation) ? node : delegation;
const char *parents[16];
int parent_count = 0;
uint32_t dnc = ts_node_named_child_count(dcontainer);
for (uint32_t di = 0; di < dnc && parent_count < 15; di++) {
TSNode dc = ts_node_named_child(dcontainer, di);
if (!kt_node_is(dc, "delegation_specifier")) {
continue;
}
TSNode ut = kt_find_descendant_kind(dc, "user_type", 4);
if (!ts_node_is_null(ut)) {
char *t = kt_node_text(ctx, ut);
if (t) {
char *lt = strchr(t, '<');
if (lt) {
*lt = '\0';
}
const char *resolved = kotlin_resolve_class_name(ctx, t);
if (resolved) {
parents[parent_count++] = resolved;
}
}
}
}
if (parent_count > 0) {
const char **embedded = (const char **)cbm_arena_alloc(
ctx->arena, sizeof(const char *) * (size_t)(parent_count + 1));
if (embedded) {
for (int p = 0; p < parent_count; p++) {
embedded[p] = parents[p];
}
embedded[parent_count] = NULL;
rt.embedded_types = embedded;
}
}
}
rt.is_object = true; /* object / companion object → static-like member calls */
cbm_registry_add_type((CBMTypeRegistry *)ctx->registry, rt);
/* Recurse into body */
TSNode body = kt_child_kind(node, "class_body");
if (ts_node_is_null(body)) {
body = kt_child_kind(node, "enum_class_body");
}
if (!ts_node_is_null(body)) {
const char *prev_class = ctx->enclosing_class_qn;
const char *prev_companion = ctx->enclosing_companion_qn;
ctx->enclosing_class_qn = obj_qn;
if (is_companion) {
ctx->enclosing_companion_qn = obj_qn;
}
kt_register_class_members(ctx, obj_qn, body, true);
ctx->enclosing_class_qn = prev_class;
ctx->enclosing_companion_qn = prev_companion;
}
}
static void kt_register_class_members(KotlinLSPContext *ctx, const char *class_qn, TSNode body,
bool is_object) {
(void)is_object;
if (ts_node_is_null(body)) {
return;
}
uint32_t nc = ts_node_named_child_count(body);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(body, i);
const char *kind = ts_node_type(c);
if (strcmp(kind, "function_declaration") == 0) {
/* Method */
TSNode name = kt_field_named(c, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(c);
}
char *fname = kt_node_text(ctx, name);
if (!fname) {
continue;
}
CBMRegisteredFunc rf = {0};
rf.qualified_name = kt_join_dot(ctx->arena, class_qn, fname);
rf.short_name = fname;
rf.receiver_type = class_qn;
rf.min_params = 0;
/* Capture return type for chain tracking. */
TSNode rt_node = kt_find_return_type(c);
if (!ts_node_is_null(rt_node)) {
const CBMType *rt = kotlin_parse_type_node(ctx, rt_node);
rf.signature = kt_build_func_sig_with_return(ctx, rt);
}
/* DSL receiver detection: scan the function's parameters for
* a function_type node whose first child is a `user_type`
* (the receiver, as in `Foo.() -> Unit`). Stash the receiver
* QN in decorator_qns[0] with a `lambda_receiver:` prefix
* so kt_process_call_with_lambda can pick it up at the call
* site and propagate it as the lambda's `this`. */
{
TSNode params = kt_child_kind(c, "function_value_parameters");
if (!ts_node_is_null(params)) {
uint32_t pnc = ts_node_named_child_count(params);
for (uint32_t pi = 0; pi < pnc; pi++) {
TSNode p = ts_node_named_child(params, pi);
if (!kt_node_is(p, "parameter")) {
continue;
}
TSNode tn = kt_field_named(p, "type");
if (ts_node_is_null(tn)) {
tn = kt_child_kind(p, "type");
}
if (ts_node_is_null(tn)) {
tn = kt_child_kind(p, "function_type");
}
TSNode ft = kt_node_is(tn, "function_type")
? tn
: kt_find_descendant_kind(tn, "function_type", 3);
if (ts_node_is_null(ft)) {
continue;
}
TSNode recv = kt_child_kind_named(ft, "user_type");
if (ts_node_is_null(recv)) {
continue;
}
char *rt_text = kt_node_text(ctx, recv);
if (!rt_text) {
continue;
}
const char *resolved = kotlin_resolve_class_name(ctx, rt_text);
if (!resolved) {
continue;
}
const char **dq =
(const char **)cbm_arena_alloc(ctx->arena, 2 * sizeof(const char *));
if (dq) {
char *tag = (char *)cbm_arena_alloc(
ctx->arena, strlen("lambda_receiver:") + strlen(resolved) + 1);
if (tag) {
strcpy(tag, "lambda_receiver:");
strcat(tag, resolved);
dq[0] = tag;
dq[1] = NULL;
rf.decorator_qns = dq;
}
}
break;
}
}
}
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, rf);
} else if (strcmp(kind, "property_declaration") == 0) {
/* Property — we don't track types deeply here, just register existence */
(void)c;
} else if (strcmp(kind, "object_declaration") == 0) {
/* Could be `companion object` or nested `object` */
char *otext = kt_node_text(ctx, c);
bool is_comp = otext && strstr(otext, "companion");
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = class_qn;
kt_process_object_decl(ctx, c, is_comp, class_qn);
ctx->enclosing_class_qn = prev_class;
} else if (strcmp(kind, "companion_object") == 0) {
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = class_qn;
kt_process_object_decl(ctx, c, true, class_qn);
ctx->enclosing_class_qn = prev_class;
} else if (strcmp(kind, "class_declaration") == 0) {
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = class_qn;
kt_process_class_decl(ctx, c);
ctx->enclosing_class_qn = prev_class;
} else if (strcmp(kind, "secondary_constructor") == 0) {
CBMRegisteredFunc rf = {0};
rf.qualified_name = kt_join_dot(ctx->arena, class_qn, "<init>");
rf.short_name = "<init>";
rf.receiver_type = class_qn;
rf.min_params = 0;
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, rf);
} else if (strcmp(kind, "enum_entry") == 0) {
/* Enum entry — register a "field" of the enum class */
}
}
}
static void kt_process_function_decl(KotlinLSPContext *ctx, TSNode node) {
TSNode name = kt_field_named(node, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(node);
}
if (ts_node_is_null(name)) {
return;
}
char *fname = kt_node_text(ctx, name);
if (!fname) {
return;
}
/* Detect extension function via the new helper which walks children
* for a type-shaped node preceding the name. Handles the inlined
* `_receiver_type` rule that tree-sitter doesn't expose as a named
* child. */
char *recv_text = NULL;
TSNode receiver = kt_find_extension_receiver(ctx, node, name, &recv_text);
(void)receiver;
if (recv_text) {
char *lt = strchr(recv_text, '<');
if (lt) {
*lt = '\0';
}
size_t rlen = strlen(recv_text);
while (rlen > 0 && (recv_text[rlen - 1] == '?' || recv_text[rlen - 1] == ' ' ||
recv_text[rlen - 1] == '\t' || recv_text[rlen - 1] == '\n')) {
recv_text[--rlen] = '\0';
}
}
CBMRegisteredFunc rf = {0};
rf.short_name = fname;
rf.min_params = 0;
if (recv_text && *recv_text) {
const char *recv_qn = kotlin_resolve_class_name(ctx, recv_text);
rf.receiver_type = recv_qn;
rf.qualified_name = kt_join_dot(ctx->arena, ctx->package_qn, fname);
} else if (ctx->enclosing_class_qn) {
rf.receiver_type = ctx->enclosing_class_qn;
rf.qualified_name = kt_join_dot(ctx->arena, ctx->enclosing_class_qn, fname);
} else {
rf.qualified_name = kt_join_dot(ctx->arena, ctx->package_qn, fname);
}
/* Capture return type for chain tracking. */
TSNode rt_node = kt_find_return_type(node);
if (!ts_node_is_null(rt_node)) {
const CBMType *rt = kotlin_parse_type_node(ctx, rt_node);
rf.signature = kt_build_func_sig_with_return(ctx, rt);
}
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, rf);
}
static void kt_process_property_decl(KotlinLSPContext *ctx, TSNode node) {
/* property_declaration: ('val'|'var') variable_declaration ('=' expr)? */
TSNode var = kt_child_kind(node, "variable_declaration");
if (ts_node_is_null(var)) {
return;
}
/* The variable_declaration's identifier becomes a top-level binding. */
TSNode id = kt_name_child(var);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(var, "simple_identifier");
}
if (ts_node_is_null(id)) {
return;
}
char *pname = kt_node_text(ctx, id);
if (!pname) {
return;
}
/* Type annotation? */
TSNode type_node = kt_child_kind(var, "type");
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "user_type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "nullable_type");
}
const CBMType *pt = cbm_type_unknown();
if (!ts_node_is_null(type_node)) {
pt = kotlin_parse_type_node(ctx, type_node);
} else {
/* Try inferring from the initializer */
TSNode init = kt_field_named(node, "value");
if (ts_node_is_null(init)) {
/* Last named child may be the initializer */
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
TSNode last = ts_node_named_child(node, nc - 1);
const char *kk = ts_node_type(last);
if (strstr(kk, "expression") || strstr(kk, "literal") ||
strcmp(kk, "call_expression") == 0 ||
strcmp(kk, "navigation_expression") == 0 || strcmp(kk, "lambda_literal") == 0) {
init = last;
}
}
}
if (!ts_node_is_null(init)) {
pt = kotlin_eval_expr_type(ctx, init);
}
}
/* Bind into file scope so top-level-property references resolve. */
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, pname), pt);
}
/* ── type parsing ─────────────────────────────────────────────────── */
const CBMType *kotlin_parse_type_node(KotlinLSPContext *ctx, TSNode node) {
if (ts_node_is_null(node)) {
return cbm_type_unknown();
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "type") == 0) {
/* Unwrap to inner type */
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
return kotlin_parse_type_node(ctx, ts_node_named_child(node, 0));
}
}
if (strcmp(kind, "nullable_type") == 0) {
TSNode inner = ts_node_named_child(node, 0);
return kotlin_parse_type_node(ctx, inner);
}
if (strcmp(kind, "non_nullable_type") == 0) {
TSNode inner = ts_node_named_child(node, 0);
return kotlin_parse_type_node(ctx, inner);
}
if (strcmp(kind, "parenthesized_type") == 0) {
TSNode inner = ts_node_named_child(node, 0);
return kotlin_parse_type_node(ctx, inner);
}
if (strcmp(kind, "function_type") == 0) {
/* (A, B) -> R — represent as CALLABLE for completeness */
return cbm_type_unknown();
}
if (strcmp(kind, "user_type") == 0 || strcmp(kind, "_simple_user_type") == 0) {
return kt_eval_user_type(ctx, node);
}
/* Fallback: textual */
char *txt = kt_node_text(ctx, node);
if (!txt) {
return cbm_type_unknown();
}
/* Strip generics and nullability */
char *lt = strchr(txt, '<');
if (lt) {
*lt = '\0';
}
size_t tl = strlen(txt);
while (tl > 0 && txt[tl - 1] == '?') {
txt[--tl] = '\0';
}
/* Trim whitespace */
while (*txt == ' ' || *txt == '\t') {
txt++;
}
const char *resolved = kotlin_resolve_class_name(ctx, txt);
if (resolved) {
return cbm_type_named(ctx->arena, resolved);
}
return cbm_type_named(ctx->arena, txt);
}
static const CBMType *kt_eval_user_type(KotlinLSPContext *ctx, TSNode node) {
/* user_type: simple_user_type ('.' simple_user_type)*
* simple_user_type: identifier type_arguments? */
char *txt = kt_node_text(ctx, node);
if (!txt) {
return cbm_type_unknown();
}
/* Strip generics */
char *lt = strchr(txt, '<');
if (lt) {
*lt = '\0';
}
/* Strip whitespace */
char *clean = (char *)cbm_arena_alloc(ctx->arena, strlen(txt) + 1);
int oi = 0;
for (int i = 0; txt[i]; i++) {
if (txt[i] != ' ' && txt[i] != '\t' && txt[i] != '\n') {
clean[oi++] = txt[i];
}
}
clean[oi] = '\0';
if (oi == 0) {
return cbm_type_unknown();
}
const char *resolved = kotlin_resolve_class_name(ctx, clean);
if (!resolved) {
return cbm_type_named(ctx->arena, clean);
}
return cbm_type_named(ctx->arena, resolved);
}
/* ── expression type inference ────────────────────────────────────── */
static const CBMType *kt_eval_literal_type(KotlinLSPContext *ctx, TSNode node) {
const char *kind = ts_node_type(node);
if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "multiline_string_literal") == 0) {
return cbm_type_named(ctx->arena, "kotlin.String");
}
if (strcmp(kind, "character_literal") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Char");
}
if (strcmp(kind, "number_literal") == 0) {
char *txt = kt_node_text(ctx, node);
if (!txt) {
return cbm_type_named(ctx->arena, "kotlin.Int");
}
size_t l = strlen(txt);
if (l == 0) {
return cbm_type_named(ctx->arena, "kotlin.Int");
}
char tail = (char)tolower((unsigned char)txt[l - 1]);
if (tail == 'l') {
return cbm_type_named(ctx->arena, "kotlin.Long");
}
if (tail == 'f') {
return cbm_type_named(ctx->arena, "kotlin.Float");
}
if (strchr(txt, '.') || strchr(txt, 'e') || strchr(txt, 'E')) {
return cbm_type_named(ctx->arena, "kotlin.Double");
}
return cbm_type_named(ctx->arena, "kotlin.Int");
}
if (strcmp(kind, "float_literal") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Double");
}
if (strcmp(kind, "boolean_literal") == 0) {
return cbm_type_named(ctx->arena, "kotlin.Boolean");
}
if (strcmp(kind, "null_literal") == 0 ||
(kt_node_text(ctx, node) && strcmp(kt_node_text(ctx, node), "null") == 0)) {
return cbm_type_named(ctx->arena, "kotlin.Nothing");
}
return cbm_type_unknown();
}
const CBMType *kotlin_eval_expr_type(KotlinLSPContext *ctx, TSNode node) {
if (ts_node_is_null(node) || !ctx) {
return cbm_type_unknown();
}
if (ctx->eval_depth >= KT_EVAL_MAX_DEPTH) {
return cbm_type_unknown();
}
ctx->eval_depth++;
const CBMType *result = cbm_type_unknown();
const char *kind = ts_node_type(node);
if (strcmp(kind, "expression") == 0 || strcmp(kind, "primary_expression") == 0 ||
strcmp(kind, "parenthesized_expression") == 0 ||
strcmp(kind, "annotated_expression") == 0 || strcmp(kind, "labeled_expression") == 0) {
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
result = kotlin_eval_expr_type(ctx, ts_node_named_child(node, 0));
}
goto out;
}
if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "multiline_string_literal") == 0 ||
strcmp(kind, "character_literal") == 0 || strcmp(kind, "number_literal") == 0 ||
strcmp(kind, "float_literal") == 0 || strcmp(kind, "boolean_literal") == 0) {
result = kt_eval_literal_type(ctx, node);
goto out;
}
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0) {
char *name = kt_node_text(ctx, node);
if (!name) {
goto out;
}
if (strcmp(name, "true") == 0 || strcmp(name, "false") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.Boolean");
goto out;
}
if (strcmp(name, "null") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.Nothing");
goto out;
}
if (strcmp(name, "it") == 0 && ctx->it_type) {
result = ctx->it_type;
goto out;
}
/* Scope lookup */
const CBMType *t = cbm_scope_lookup(ctx->current_scope, name);
if (!cbm_type_is_unknown(t)) {
result = t;
goto out;
}
/* Maybe a class name (object reference) */
const char *cls_qn = kotlin_resolve_class_name(ctx, name);
if (cls_qn) {
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, cls_qn);
if (rt) {
result = cbm_type_named(ctx->arena, cls_qn);
goto out;
}
}
/* Maybe top-level property */
goto out;
}
if (strcmp(kind, "this_expression") == 0) {
result = ctx->this_type ? ctx->this_type : cbm_type_unknown();
goto out;
}
if (strcmp(kind, "super_expression") == 0) {
result = ctx->super_type ? ctx->super_type : cbm_type_unknown();
goto out;
}
if (strcmp(kind, "call_expression") == 0) {
result = kt_eval_call_expression_type(ctx, node);
goto out;
}
if (strcmp(kind, "navigation_expression") == 0) {
result = kt_eval_navigation_expression_type(ctx, node);
goto out;
}
if (strcmp(kind, "as_expression") == 0) {
/* obj as Foo / obj as? Foo — result is Foo */
TSNode rhs = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
result = kotlin_parse_type_node(ctx, rhs);
goto out;
}
if (strcmp(kind, "is_expression") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.Boolean");
goto out;
}
if (strcmp(kind, "binary_expression") == 0 || strcmp(kind, "additive_expression") == 0 ||
strcmp(kind, "multiplicative_expression") == 0 ||
strcmp(kind, "comparison_expression") == 0 || strcmp(kind, "equality_expression") == 0 ||
strcmp(kind, "range_expression") == 0) {
/* Operator-convention dispatch: Kotlin desugars `a OP b` to
* `a.<method>(b)` for a fixed set of operators. We detect the
* operator token, evaluate `a`'s type, and emit a method-call
* edge — matching what the official Kotlin compiler frontend
* does in BindingContext.RESOLVED_CALL. Newer tree-sitter-kotlin
* splits the old `binary_expression` into precedence-specific nodes. */
TSNode lhs = kt_field_named(node, "left");
TSNode rhs = kt_field_named(node, "right");
if (ts_node_is_null(lhs) && ts_node_named_child_count(node) >= 1) {
lhs = ts_node_named_child(node, 0);
}
if (ts_node_is_null(rhs) && ts_node_named_child_count(node) >= 2) {
rhs = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
}
const CBMType *lhs_t = kotlin_eval_expr_type(ctx, lhs);
/* The operator token is between named children; extract from
* source range. */
char *full = kt_node_text(ctx, node);
const char *op_method = NULL;
if (full) {
/* Common operator → method mapping. We check the FIRST
* occurrence of each token after the lhs end and before
* the rhs start. As a simple heuristic we check the raw
* source text for canonical operator strings; the order
* matters to disambiguate `==` vs `=` and `..` vs `.`. */
uint32_t lhs_end = ts_node_is_null(lhs) ? 0 : ts_node_end_byte(lhs);
uint32_t rhs_start =
ts_node_is_null(rhs) ? ts_node_end_byte(node) : ts_node_start_byte(rhs);
uint32_t node_start = ts_node_start_byte(node);
const char *between = ctx->source + lhs_end;
int between_len = (int)(rhs_start - lhs_end);
if (between_len > 0 && lhs_end > node_start) {
/* Check operators in disambiguation order. */
if (cbm_memmem(between, (size_t)between_len, "===", 3)) {
op_method = NULL; /* identity, no method */
} else if (cbm_memmem(between, (size_t)between_len, "!==", 3)) {
op_method = NULL;
} else if (cbm_memmem(between, (size_t)between_len, "==", 2)) {
op_method = "equals";
} else if (cbm_memmem(between, (size_t)between_len, "!=", 2)) {
op_method = "equals";
} else if (cbm_memmem(between, (size_t)between_len, "..<", 3)) {
op_method = "rangeUntil";
} else if (cbm_memmem(between, (size_t)between_len, "..", 2)) {
op_method = "rangeTo";
} else if (cbm_memmem(between, (size_t)between_len, "+=", 2)) {
op_method = "plusAssign";
} else if (cbm_memmem(between, (size_t)between_len, "-=", 2)) {
op_method = "minusAssign";
} else if (cbm_memmem(between, (size_t)between_len, "*=", 2)) {
op_method = "timesAssign";
} else if (cbm_memmem(between, (size_t)between_len, "/=", 2)) {
op_method = "divAssign";
} else if (cbm_memmem(between, (size_t)between_len, "%=", 2)) {
op_method = "remAssign";
} else if (cbm_memmem(between, (size_t)between_len, "<=", 2) ||
cbm_memmem(between, (size_t)between_len, ">=", 2) ||
cbm_memmem(between, (size_t)between_len, "<", 1) ||
cbm_memmem(between, (size_t)between_len, ">", 1)) {
op_method = "compareTo";
} else if (cbm_memmem(between, (size_t)between_len, "+", 1)) {
op_method = "plus";
} else if (cbm_memmem(between, (size_t)between_len, "-", 1)) {
op_method = "minus";
} else if (cbm_memmem(between, (size_t)between_len, "*", 1)) {
op_method = "times";
} else if (cbm_memmem(between, (size_t)between_len, "/", 1)) {
op_method = "div";
} else if (cbm_memmem(between, (size_t)between_len, "%", 1)) {
op_method = "rem";
}
}
(void)node_start;
}
if (op_method && lhs_t && !cbm_type_is_unknown(lhs_t)) {
const char *lhs_qn = kt_type_qn_of(lhs_t);
if (lhs_qn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, lhs_qn, op_method);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_operator", KT_CONF_METHOD);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
result = rf->signature->data.func.return_types[0];
goto out;
}
}
}
}
/* Default: type is LHS type (numeric ops, etc.) */
result = lhs_t;
goto out;
}
if (strcmp(kind, "unary_expression") == 0 || strcmp(kind, "prefix_expression") == 0 ||
strcmp(kind, "postfix_expression") == 0) {
/* Unary operator desugars to method call:
* +x → x.unaryPlus(), -x → x.unaryMinus(),
* !x → x.not(), ++x → x.inc(), --x → x.dec()
* Newer tree-sitter-kotlin uses prefix_expression/postfix_expression. */
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
TSNode operand = ts_node_named_child(node, nc - 1);
const CBMType *t = kotlin_eval_expr_type(ctx, operand);
char *full = kt_node_text(ctx, node);
const char *op_method = NULL;
if (full) {
/* Look at chars before operand */
uint32_t op_end = ts_node_start_byte(operand);
uint32_t node_start = ts_node_start_byte(node);
if (op_end > node_start) {
const char *prefix = ctx->source + node_start;
int prefix_len = (int)(op_end - node_start);
if (cbm_memmem(prefix, (size_t)prefix_len, "++", 2)) {
op_method = "inc";
} else if (cbm_memmem(prefix, (size_t)prefix_len, "--", 2)) {
op_method = "dec";
} else if (cbm_memmem(prefix, (size_t)prefix_len, "!", 1)) {
op_method = "not";
} else if (cbm_memmem(prefix, (size_t)prefix_len, "-", 1)) {
op_method = "unaryMinus";
} else if (cbm_memmem(prefix, (size_t)prefix_len, "+", 1)) {
op_method = "unaryPlus";
}
}
}
if (op_method && t && !cbm_type_is_unknown(t)) {
const char *qn = kt_type_qn_of(t);
if (qn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, qn, op_method);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_operator",
KT_CONF_METHOD);
}
}
}
result = t;
}
goto out;
}
if (strcmp(kind, "in_expression") == 0 || strcmp(kind, "check_expression") == 0) {
/* `a in b` desugars to `b.contains(a)`; `a is T` is a Boolean type
* check (no method). Newer tree-sitter-kotlin emits both as
* `check_expression`; disambiguate via the operator token. */
char *ntext = kt_node_text(ctx, node);
bool is_membership =
ntext && (cbm_memmem(ntext, strlen(ntext), " in ", 4) || strstr(ntext, "!in") != NULL);
uint32_t nc = ts_node_named_child_count(node);
if (is_membership && nc >= 2) {
TSNode container = ts_node_named_child(node, nc - 1);
const CBMType *ct = kotlin_eval_expr_type(ctx, container);
const char *cqn = kt_type_qn_of(ct);
if (cqn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, cqn, "contains");
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_operator", KT_CONF_METHOD);
}
}
}
result = cbm_type_named(ctx->arena, "kotlin.Boolean");
goto out;
}
if (strcmp(kind, "index_expression") == 0 || strcmp(kind, "indexing_expression") == 0) {
/* `a[i]` desugars to `a.get(i)`. */
uint32_t nc = ts_node_named_child_count(node);
if (nc >= 1) {
TSNode recv = ts_node_named_child(node, 0);
const CBMType *rt = kotlin_eval_expr_type(ctx, recv);
const char *rqn = kt_type_qn_of(rt);
if (rqn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, rqn, "get");
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_operator", KT_CONF_METHOD);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
result = rf->signature->data.func.return_types[0];
goto out;
}
}
}
}
result = cbm_type_unknown();
goto out;
}
if (strcmp(kind, "callable_reference") == 0) {
/* Callable references take three shapes:
* `::topLevelFn` — bound to receiverless top-level function
* `Foo::method` — bound class method or property
* `instance::method`— bound to instance method
* `Foo::class` — class literal (KClass<Foo>)
* `T::class` (reified) — class literal of reified type param
*
* We emit an edge with strategy `lsp_kt_callable_ref` so the call
* graph captures the reference even when it's not invoked here.
* The grammar exposes the LHS (optional) and RHS via named
* children — typically two identifiers separated by `::`. */
uint32_t nc = ts_node_named_child_count(node);
if (nc >= 1) {
TSNode last = ts_node_named_child(node, nc - 1);
char *member = kt_node_text(ctx, last);
if (member) {
if (nc >= 2) {
TSNode lhs = ts_node_named_child(node, 0);
/* lhs may be a type or expression. Try type-resolve
* first (for `Foo::method`), then expression eval
* (for `obj::method`). */
char *lhs_text = kt_node_text(ctx, lhs);
const char *recv_qn = NULL;
if (lhs_text) {
recv_qn = kotlin_resolve_class_name(ctx, lhs_text);
}
if (!recv_qn) {
const CBMType *t = kotlin_eval_expr_type(ctx, lhs);
recv_qn = kt_type_qn_of(t);
}
if (recv_qn) {
if (strcmp(member, "class") == 0) {
/* `Foo::class` produces KClass<Foo>. We track
* the reference but don't emit an edge — it's
* a literal, not a call. */
result = cbm_type_named(ctx->arena, "kotlin.reflect.KClass");
goto out;
}
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, recv_qn, member);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_callable_ref",
KT_CONF_METHOD);
}
}
} else {
/* `::name` — bound to top-level fn or local. */
const char *fn_qn = kotlin_resolve_function_name(ctx, member);
if (fn_qn) {
kt_emit_resolved(ctx, fn_qn, "lsp_kt_callable_ref", KT_CONF_TOP_LEVEL);
}
}
}
}
result = cbm_type_unknown();
goto out;
}
if (strcmp(kind, "if_expression") == 0) {
/* Pick first branch type */
TSNode then_b = kt_field_named(node, "then");
if (!ts_node_is_null(then_b)) {
result = kotlin_eval_expr_type(ctx, then_b);
goto out;
}
}
if (strcmp(kind, "when_expression") == 0) {
/* Find first when_entry's body */
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(node, i);
if (kt_node_is(c, "when_entry")) {
/* Last named child is the body expression */
uint32_t en = ts_node_named_child_count(c);
if (en > 0) {
TSNode body = ts_node_named_child(c, en - 1);
result = kotlin_eval_expr_type(ctx, body);
if (!cbm_type_is_unknown(result)) {
goto out;
}
}
}
}
}
if (strcmp(kind, "lambda_literal") == 0 || strcmp(kind, "annotated_lambda") == 0 ||
strcmp(kind, "anonymous_function") == 0) {
/* Lambda — result type is unknown without context. */
result = cbm_type_unknown();
goto out;
}
if (strcmp(kind, "object_literal") == 0) {
/* Anonymous object — closest thing is the inferred parent type */
TSNode delegation = kt_child_kind(node, "delegation_specifiers");
if (!ts_node_is_null(delegation)) {
TSNode ut = kt_find_descendant_kind(delegation, "user_type", 5);
if (!ts_node_is_null(ut)) {
result = kt_eval_user_type(ctx, ut);
goto out;
}
}
result = cbm_type_named(ctx->arena, "kotlin.Any");
goto out;
}
if (strcmp(kind, "collection_literal") == 0) {
/* [a, b, c] — typically a List */
result = cbm_type_named(ctx->arena, "kotlin.collections.List");
goto out;
}
if (strcmp(kind, "range_expression") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.ranges.IntRange");
goto out;
}
if (strcmp(kind, "throw_expression") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.Nothing");
goto out;
}
if (strcmp(kind, "return_expression") == 0) {
result = cbm_type_named(ctx->arena, "kotlin.Nothing");
goto out;
}
/* Fallthrough: unwrap single-named-child wrappers */
{
uint32_t nc = ts_node_named_child_count(node);
if (nc == 1) {
result = kotlin_eval_expr_type(ctx, ts_node_named_child(node, 0));
}
}
out:
ctx->eval_depth--;
return result ? result : cbm_type_unknown();
}
static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TSNode call_node,
const char *callee_text) {
/* callee_text is a bare identifier — it may be:
* 1) A class constructor: `Foo(...)` → returns Foo
* 2) A top-level function: `foo(...)` → returns its declared return
* 3) A scope binding (function reference held in a val): use type
* 4) `it(arg)` if `it` is callable
*/
if (!callee_text) {
return cbm_type_unknown();
}
/* 0. `this`-method dispatch: if we're inside a class method or DSL
* lambda, bare calls resolve against this_type's members first. This
* matches Kotlin's resolution order: implicit-receiver before global. */
if (ctx->this_type && !cbm_type_is_unknown(ctx->this_type)) {
const char *this_qn = kt_type_qn_of(ctx->this_type);
if (this_qn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, this_qn, callee_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_this", KT_CONF_THIS);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
return cbm_type_unknown();
}
}
}
/* 1. Class? */
const char *cls_qn = kotlin_resolve_class_name(ctx, callee_text);
if (cls_qn && ctx->registry) {
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, cls_qn);
if (rt) {
/* A constructor call `Foo()` resolves to the Foo CLASS node, which the
* textual extractor stored; there is no separate `Foo.<init>` graph
* node, and the textual call site's callee is the bare class name
* `Foo` (not `<init>`). Emitting cls_qn (not cls_qn.<init>) makes the
* pipeline join's callee bare-segment match AND resolves the target. */
kt_emit_resolved(ctx, cls_qn, "lsp_kt_constructor", KT_CONF_CONSTRUCTOR);
return cbm_type_named(ctx->arena, cls_qn);
}
}
/* 2. Top-level fun */
const char *fn_qn = kotlin_resolve_function_name(ctx, callee_text);
if (fn_qn) {
kt_emit_resolved(ctx, fn_qn, "lsp_kt_top_level", KT_CONF_TOP_LEVEL);
if (ctx->registry) {
const CBMRegisteredFunc *rf = cbm_registry_lookup_func(ctx->registry, fn_qn);
if (rf && rf->signature && rf->signature->kind == CBM_TYPE_FUNC) {
if (rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
}
}
/* Heuristic return types for stdlib top-level builders. The
* curated stdlib registers these by name only (no signature)
* to keep the data table compact; we provide return types here
* so chains like `mutableListOf<X>().filter { … }` propagate. */
struct {
const char *qn;
const char *ret;
} stdlib_returns[] = {
{"kotlin.collections.listOf", "kotlin.collections.List"},
{"kotlin.collections.listOfNotNull", "kotlin.collections.List"},
{"kotlin.collections.mutableListOf", "kotlin.collections.MutableList"},
{"kotlin.collections.arrayListOf", "kotlin.collections.ArrayList"},
{"kotlin.collections.emptyList", "kotlin.collections.List"},
{"kotlin.collections.setOf", "kotlin.collections.Set"},
{"kotlin.collections.mutableSetOf", "kotlin.collections.MutableSet"},
{"kotlin.collections.hashSetOf", "kotlin.collections.HashSet"},
{"kotlin.collections.linkedSetOf", "kotlin.collections.LinkedHashSet"},
{"kotlin.collections.emptySet", "kotlin.collections.Set"},
{"kotlin.collections.mapOf", "kotlin.collections.Map"},
{"kotlin.collections.mutableMapOf", "kotlin.collections.MutableMap"},
{"kotlin.collections.hashMapOf", "kotlin.collections.HashMap"},
{"kotlin.collections.linkedMapOf", "kotlin.collections.LinkedHashMap"},
{"kotlin.collections.emptyMap", "kotlin.collections.Map"},
{"kotlin.arrayOf", "kotlin.Array"},
{"kotlin.arrayOfNulls", "kotlin.Array"},
{"kotlin.emptyArray", "kotlin.Array"},
{"kotlin.intArrayOf", "kotlin.IntArray"},
{"kotlin.longArrayOf", "kotlin.LongArray"},
{"kotlin.floatArrayOf", "kotlin.FloatArray"},
{"kotlin.doubleArrayOf", "kotlin.DoubleArray"},
{"kotlin.byteArrayOf", "kotlin.ByteArray"},
{"kotlin.booleanArrayOf", "kotlin.BooleanArray"},
{"kotlin.charArrayOf", "kotlin.CharArray"},
{"kotlin.sequences.sequenceOf", "kotlin.sequences.Sequence"},
{"kotlin.sequences.emptySequence", "kotlin.sequences.Sequence"},
{"kotlin.sequences.generateSequence", "kotlin.sequences.Sequence"},
{"kotlin.sequences.sequence", "kotlin.sequences.Sequence"},
{"kotlin.io.readLine", "kotlin.String"},
{"kotlin.io.readln", "kotlin.String"},
{"kotlin.text.buildString", "kotlin.String"},
{"kotlin.lazy", "kotlin.Lazy"},
{"kotlin.lazyOf", "kotlin.Lazy"},
{"kotlin.runCatching", "kotlin.Result"},
{"kotlin.to", "kotlin.Pair"},
{NULL, NULL},
};
for (int i = 0; stdlib_returns[i].qn; i++) {
if (strcmp(stdlib_returns[i].qn, fn_qn) == 0) {
return cbm_type_named(ctx->arena, stdlib_returns[i].ret);
}
}
}
(void)call_node;
return cbm_type_unknown();
}
static const CBMType *kt_eval_call_expression_type(KotlinLSPContext *ctx, TSNode node) {
/* call_expression: <expr> value_arguments (annotated_lambda)? */
/* The first named child is the callee expression. */
uint32_t nc = ts_node_named_child_count(node);
if (nc == 0) {
return cbm_type_unknown();
}
TSNode callee = ts_node_named_child(node, 0);
const char *kind = ts_node_type(callee);
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0) {
char *name = kt_node_text(ctx, callee);
return kt_eval_constructor_or_func_call(ctx, node, name);
}
if (strcmp(kind, "navigation_expression") == 0) {
/* obj.method(...) — inner type evaluation handles emission */
const CBMType *t = kt_eval_navigation_expression_type(ctx, callee);
return t;
}
/* Could also be `call_expression` for chained calls like foo()(args) */
return kotlin_eval_expr_type(ctx, callee);
}
/* Extract the member-name node from a navigation_expression. Newer
* tree-sitter-kotlin wraps the member in a `navigation_suffix` node
* (`. simple_identifier`); older grammars placed the simple_identifier
* directly as the trailing named child. */
static TSNode kt_nav_member_node(TSNode nav) {
uint32_t nc = ts_node_named_child_count(nav);
if (nc == 0) {
TSNode z;
memset(&z, 0, sizeof(z));
return z;
}
TSNode sel = ts_node_named_child(nav, nc - 1);
if (kt_node_is(sel, "navigation_suffix")) {
TSNode inner = kt_child_kind_named(sel, "simple_identifier");
if (!ts_node_is_null(inner)) {
return inner;
}
}
return sel;
}
static const CBMType *kt_eval_navigation_expression_type(KotlinLSPContext *ctx, TSNode node) {
/* navigation_expression: <expr> ('.'|'?.'|'!!.') (simple_identifier | navigation_suffix) */
uint32_t nc = ts_node_named_child_count(node);
if (nc < 2) {
return cbm_type_unknown();
}
TSNode receiver_node = ts_node_named_child(node, 0);
TSNode selector = kt_nav_member_node(node);
char *member_text = kt_node_text(ctx, selector);
if (!member_text) {
return cbm_type_unknown();
}
/* Special: super.foo */
if (kt_node_is(receiver_node, "super_expression")) {
if (ctx->enclosing_super_qn) {
const CBMRegisteredFunc *rf =
kotlin_lookup_method(ctx, ctx->enclosing_super_qn, member_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_super", KT_CONF_SUPER);
return cbm_type_unknown();
}
}
return cbm_type_unknown();
}
/* Special: this.foo */
if (kt_node_is(receiver_node, "this_expression")) {
if (ctx->enclosing_class_qn) {
const CBMRegisteredFunc *rf =
kotlin_lookup_method(ctx, ctx->enclosing_class_qn, member_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_this", KT_CONF_THIS);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
return cbm_type_unknown();
}
/* Property access? */
const CBMType *pt =
kotlin_lookup_property_type(ctx, ctx->enclosing_class_qn, member_text);
if (!cbm_type_is_unknown(pt)) {
return pt;
}
}
return cbm_type_unknown();
}
const CBMType *recv_type = kotlin_eval_expr_type(ctx, receiver_node);
recv_type = kt_unwrap_nullable(recv_type);
const char *recv_qn = kt_type_qn_of(recv_type);
/* Receiver might be a class reference itself — `Foo.bar()` where Foo is
* a singleton object or an enum class with a companion. */
if (recv_qn && ctx->registry) {
/* Check object-singleton or companion lookup */
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, recv_qn, member_text);
if (rf && rf->qualified_name) {
/* Distinguish an extension function from a member method: a member's
* QN nests under the receiver (`<recv_qn>.<member>`), while an
* extension `fun Recv.ext()` is a TOP-LEVEL fun whose QN does NOT
* nest under recv_qn (only its receiver_type points back).
* kotlin_lookup_method matches both, so pick the strategy by QN shape. */
size_t recv_len = strlen(recv_qn);
bool is_member = (strncmp(rf->qualified_name, recv_qn, recv_len) == 0 &&
rf->qualified_name[recv_len] == '.');
const char *strat = "lsp_kt_extension";
if (is_member) {
/* A member call on an `object`/`companion object` singleton is a
* static dispatch; on a regular class instance it is a method. */
const CBMRegisteredType *recv_rt =
cbm_registry_lookup_type(ctx->registry, recv_qn);
strat = (recv_rt && recv_rt->is_object) ? "lsp_kt_static" : "lsp_kt_method";
}
/* A call through the lambda implicit parameter `it` (e.g. inside
* `x.let { it.m() }`) is lambda-scoped dispatch, not a plain method. */
if (kt_node_is(receiver_node, "identifier") ||
kt_node_is(receiver_node, "simple_identifier")) {
char *rtext = kt_node_text(ctx, receiver_node);
if (rtext && strcmp(rtext, "it") == 0) {
strat = "lsp_kt_lambda_it";
}
}
kt_emit_resolved(ctx, rf->qualified_name, strat, KT_CONF_METHOD);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types && rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
/* Heuristic: stdlib self-returning / known-result methods. */
const CBMType *guess = kt_stdlib_method_return_type(ctx, recv_qn, member_text);
if (!cbm_type_is_unknown(guess)) {
return guess;
}
return cbm_type_unknown();
}
/* Companion fallback: receiver is class Foo, lookup Foo.Companion.<member> */
char *companion_qn = kt_join_dot(ctx->arena, recv_qn, "Companion");
rf = kotlin_lookup_method(ctx, companion_qn, member_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_static", KT_CONF_STATIC);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types && rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
return cbm_type_unknown();
}
/* Property */
const CBMType *pt = kotlin_lookup_property_type(ctx, recv_qn, member_text);
if (!cbm_type_is_unknown(pt)) {
return pt;
}
/* Extension function: search for any registered func with
* receiver_type == recv_qn and short_name == member_text. */
rf = cbm_registry_lookup_method(ctx->registry, recv_qn, member_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_extension", KT_CONF_EXTENSION);
return cbm_type_unknown();
}
}
/* Bare receiver via `it.member` when inside scope-function lambda. */
if (ctx->it_type && kt_node_is(receiver_node, "identifier")) {
char *recv_text = kt_node_text(ctx, receiver_node);
if (recv_text && strcmp(recv_text, "it") == 0) {
const char *it_qn = kt_type_qn_of(ctx->it_type);
if (it_qn) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, it_qn, member_text);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_lambda_it",
KT_CONF_LAMBDA_IT);
return cbm_type_unknown();
}
}
}
}
/* Universal-method fallback: if the receiver type is unknown but the
* member is a method that EVERY Kotlin reference has via kotlin.Any
* (toString, equals, hashCode), emit on kotlin.Any. This is what
* the fwcd LSP also resolves to when no narrower receiver is known —
* Any is the supertype of every Kotlin reference. We emit at slightly
* lower confidence (KT_CONF_PARTIAL) since we couldn't pin the exact
* override target. */
{
static const char *kt_any_methods[] = {"toString", "equals", "hashCode", NULL};
bool is_any = false;
for (int i = 0; kt_any_methods[i]; i++) {
if (strcmp(member_text, kt_any_methods[i]) == 0) {
is_any = true;
break;
}
}
if (is_any) {
char *q = kt_join_dot(ctx->arena, "kotlin.Any", member_text);
kt_emit_resolved(ctx, q, "lsp_kt_any", KT_CONF_PARTIAL);
}
}
return cbm_type_unknown();
}
/* ── statement processing ─────────────────────────────────────────── */
static void kt_process_statement(KotlinLSPContext *ctx, TSNode stmt);
static void kt_process_block_stmts(KotlinLSPContext *ctx, TSNode block) {
if (ts_node_is_null(block)) {
return;
}
uint32_t nc = ts_node_named_child_count(block);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(block, i);
kt_process_statement(ctx, c);
}
}
static void kt_bind_property_to_scope(KotlinLSPContext *ctx, TSNode prop) {
/* property_declaration: ('val'|'var') (variable_declaration | multi_variable_declaration)
* ('=' expr | property_delegate)?
*
* Handles three shapes:
* 1. `val x: T = expr` — single binding, optional type or initializer
* 2. `val (a, b) = expr` — destructuring; emits expr.component1(),
* expr.component2() per official LSP convention
* 3. `val x by lazy { ... }` — property delegation; emits delegate.getValue()
* (and delegate.setValue() for var) per the
* KProperty contract.
*/
/* Multi-variable destructuring */
TSNode multi = kt_child_kind(prop, "multi_variable_declaration");
if (!ts_node_is_null(multi)) {
/* Find the initializer expression — the last named child of `prop`
* that's not the modifiers/multi_variable_declaration. */
uint32_t nc = ts_node_named_child_count(prop);
TSNode init;
memset(&init, 0, sizeof(init));
for (uint32_t i = nc; i > 0; i--) {
TSNode c = ts_node_named_child(prop, i - 1);
const char *k = ts_node_type(c);
if (strcmp(k, "modifiers") == 0 || strcmp(k, "multi_variable_declaration") == 0) {
continue;
}
init = c;
break;
}
const CBMType *init_t = cbm_type_unknown();
if (!ts_node_is_null(init)) {
init_t = kotlin_eval_expr_type(ctx, init);
}
/* Emit componentN calls for each variable in the multi-decl. */
if (init_t && !cbm_type_is_unknown(init_t)) {
const char *iqn = kt_type_qn_of(init_t);
uint32_t mnc = ts_node_named_child_count(multi);
for (uint32_t i = 0; i < mnc; i++) {
TSNode v = ts_node_named_child(multi, i);
if (!kt_node_is(v, "variable_declaration")) {
continue;
}
if (iqn) {
char comp[16];
snprintf(comp, sizeof(comp), "component%u", i + 1);
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, iqn, comp);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_destructure",
KT_CONF_METHOD);
}
}
/* Bind variable to unknown type for now. */
TSNode vid = kt_name_child(v);
if (!ts_node_is_null(vid)) {
char *vname = kt_node_text(ctx, vid);
if (vname) {
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, vname),
cbm_type_unknown());
}
}
}
}
return;
}
TSNode var = kt_child_kind(prop, "variable_declaration");
if (ts_node_is_null(var)) {
return;
}
TSNode id = kt_name_child(var);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(var, "simple_identifier");
}
if (ts_node_is_null(id)) {
return;
}
char *name = kt_node_text(ctx, id);
if (!name) {
return;
}
/* Type annotation? */
TSNode type_node = kt_child_kind(var, "type");
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "user_type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(var, "nullable_type");
}
/* Property delegation: `val x by Foo()` — emit getValue (and setValue
* for var) on the delegate's type. */
TSNode delegate = kt_child_kind(prop, "property_delegate");
if (!ts_node_is_null(delegate)) {
/* Find inner expression of the delegate. */
uint32_t dnc = ts_node_named_child_count(delegate);
if (dnc > 0) {
TSNode inner = ts_node_named_child(delegate, 0);
const CBMType *dt = kotlin_eval_expr_type(ctx, inner);
if (dt && !cbm_type_is_unknown(dt)) {
const char *dqn = kt_type_qn_of(dt);
if (dqn) {
const CBMRegisteredFunc *gv = kotlin_lookup_method(ctx, dqn, "getValue");
if (gv && gv->qualified_name) {
kt_emit_resolved(ctx, gv->qualified_name, "lsp_kt_delegate",
KT_CONF_METHOD);
}
/* Detect var (mutable) by looking for `var` keyword in
* the property's source range. */
char *src_text = kt_node_text(ctx, prop);
if (src_text && strstr(src_text, "var ")) {
const CBMRegisteredFunc *sv = kotlin_lookup_method(ctx, dqn, "setValue");
if (sv && sv->qualified_name) {
kt_emit_resolved(ctx, sv->qualified_name, "lsp_kt_delegate",
KT_CONF_METHOD);
}
}
}
}
}
}
const CBMType *t = cbm_type_unknown();
if (!ts_node_is_null(type_node)) {
t = kotlin_parse_type_node(ctx, type_node);
} else {
/* Scan named children of the property_declaration for an
* initializer expression after the variable_declaration. */
uint32_t nc = ts_node_named_child_count(prop);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(prop, i);
const char *k = ts_node_type(c);
if (strcmp(k, "variable_declaration") == 0 || strcmp(k, "modifiers") == 0 ||
strcmp(k, "property_delegate") == 0) {
continue;
}
t = kotlin_eval_expr_type(ctx, c);
if (!cbm_type_is_unknown(t)) {
break;
}
}
}
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, name), t);
}
/* Smart-cast inside `if (x is Foo)`. Detects pattern in stmt and binds
* narrowed type into a child scope on the then-branch. */
static void kt_apply_smart_cast(KotlinLSPContext *ctx, TSNode condition_expr, bool then_branch) {
if (ts_node_is_null(condition_expr) || !then_branch) {
return;
}
/* is_expression (older) or check_expression (newer grammar): <expr> 'is'
* <type>. Only `is`/`!is` narrows the type — skip `in` membership. */
bool is_check = kt_node_is(condition_expr, "is_expression");
if (!is_check && kt_node_is(condition_expr, "check_expression")) {
char *ctext = kt_node_text(ctx, condition_expr);
if (ctext &&
(cbm_memmem(ctext, strlen(ctext), " is ", 4) != NULL || strstr(ctext, "!is") != NULL)) {
is_check = true;
}
}
if (!is_check) {
return;
}
TSNode lhs = ts_node_named_child(condition_expr, 0);
TSNode rhs = ts_node_named_child(condition_expr, ts_node_named_child_count(condition_expr) - 1);
if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) {
return;
}
if (!(kt_node_is(lhs, "identifier") || kt_node_is(lhs, "simple_identifier"))) {
return;
}
char *name = kt_node_text(ctx, lhs);
if (!name) {
return;
}
const CBMType *t = kotlin_parse_type_node(ctx, rhs);
if (!cbm_type_is_unknown(t)) {
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, name), t);
}
}
static void kt_process_if_expression(KotlinLSPContext *ctx, TSNode node) {
TSNode cond = kt_field_named(node, "condition");
if (ts_node_is_null(cond)) {
cond = kt_child_kind(node, "condition");
}
if (ts_node_is_null(cond)) {
/* parenthesized condition fallback */
cond = ts_node_named_child(node, 0);
}
TSNode then_b = kt_field_named(node, "then");
if (ts_node_is_null(then_b)) {
/* Find the first non-condition block/expression */
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 1; i < nc; i++) {
TSNode c = ts_node_named_child(node, i);
const char *k = ts_node_type(c);
if (strcmp(k, "block") == 0 || strcmp(k, "control_structure_body") == 0 ||
strstr(k, "expression")) {
then_b = c;
break;
}
}
}
if (!ts_node_is_null(then_b)) {
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
if (!ts_node_is_null(cond)) {
kt_apply_smart_cast(ctx, cond, true);
}
if (kt_node_is(then_b, "block") || kt_node_is(then_b, "control_structure_body")) {
kt_process_block_stmts(ctx, then_b);
} else {
kt_resolve_calls_in_node(ctx, then_b);
kotlin_eval_expr_type(ctx, then_b);
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
}
/* Resolve calls inside the condition itself */
if (!ts_node_is_null(cond)) {
kt_resolve_calls_in_node(ctx, cond);
}
}
static void kt_process_when_expression(KotlinLSPContext *ctx, TSNode node) {
/* when (subject) { entries } — bind subject type as `it`-like? Actually
* Kotlin's `when (x) { is Foo -> ... }` creates smart-cast on x. */
TSNode subject = kt_child_kind(node, "when_subject");
char *subject_name = NULL;
const CBMType *subject_type = NULL;
if (!ts_node_is_null(subject)) {
/* when_subject: '(' expression ')' or '(' val name = expr ')' */
TSNode inner = ts_node_named_child(subject, 0);
if (!ts_node_is_null(inner)) {
subject_type = kotlin_eval_expr_type(ctx, inner);
if (kt_node_is(inner, "identifier") || kt_node_is(inner, "simple_identifier")) {
subject_name = kt_node_text(ctx, inner);
}
}
}
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(node, i);
if (!kt_node_is(c, "when_entry")) {
continue;
}
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
/* Look for `is Type` conditions */
uint32_t en = ts_node_named_child_count(c);
for (uint32_t e = 0; e < en; e++) {
TSNode ec = ts_node_named_child(c, e);
if (kt_node_is(ec, "_when_condition") || kt_node_is(ec, "when_condition") ||
kt_node_is(ec, "type_test")) {
/* Find a 'type' node and apply smart-cast on subject_name */
TSNode tt = kt_find_descendant_kind(ec, "type", 3);
if (ts_node_is_null(tt)) {
tt = kt_find_descendant_kind(ec, "user_type", 3);
}
if (!ts_node_is_null(tt) && subject_name) {
const CBMType *narrow = kotlin_parse_type_node(ctx, tt);
if (!cbm_type_is_unknown(narrow)) {
cbm_scope_bind(ctx->current_scope,
cbm_arena_strdup(ctx->arena, subject_name), narrow);
}
}
}
}
/* Process entry body */
if (en > 0) {
TSNode body = ts_node_named_child(c, en - 1);
if (kt_node_is(body, "block")) {
kt_process_block_stmts(ctx, body);
} else {
kt_resolve_calls_in_node(ctx, body);
kotlin_eval_expr_type(ctx, body);
}
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
}
(void)subject_type;
}
static void kt_process_for_statement(KotlinLSPContext *ctx, TSNode node) {
/* for (x in iter) body
*
* Kotlin desugars this to roughly:
* val it = iter.iterator()
* while (it.hasNext()) {
* val x = it.next()
* body
* }
*
* We emit method calls for `iterator()`, `hasNext()`, and `next()`
* to match what the official LSP's BindingContext.LOOP_RANGE_*
* slices contain — these are real graph edges users care about. */
TSNode loop_var = kt_child_kind_named(node, "variable_declaration");
TSNode iter = kt_field_named(node, "iterable");
if (ts_node_is_null(iter)) {
/* Find 'in' position — iterable is the expr after 'in' */
uint32_t nc = ts_node_named_child_count(node);
if (nc >= 2) {
iter = ts_node_named_child(node, 1);
}
}
const CBMType *iter_t = kotlin_eval_expr_type(ctx, iter);
if (iter_t && !cbm_type_is_unknown(iter_t)) {
const char *iqn = kt_type_qn_of(iter_t);
if (iqn) {
const char *protocol[] = {"iterator", "hasNext", "next", NULL};
for (int i = 0; protocol[i]; i++) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, iqn, protocol[i]);
if (rf && rf->qualified_name) {
kt_emit_resolved(ctx, rf->qualified_name, "lsp_kt_iterator", KT_CONF_METHOD);
}
}
}
}
(void)iter_t;
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
if (!ts_node_is_null(loop_var)) {
TSNode id = kt_name_child(loop_var);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(loop_var, "simple_identifier");
}
if (!ts_node_is_null(id)) {
char *name = kt_node_text(ctx, id);
if (name) {
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, name),
cbm_type_unknown());
}
}
}
/* Body */
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
TSNode body = ts_node_named_child(node, nc - 1);
if (kt_node_is(body, "block")) {
kt_process_block_stmts(ctx, body);
} else {
kt_resolve_calls_in_node(ctx, body);
kotlin_eval_expr_type(ctx, body);
}
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
}
static void kt_process_lambda(KotlinLSPContext *ctx, TSNode lambda, const CBMType *receiver_type) {
/* lambda_literal: '{' lambda_parameters? '->' statements '}'
* If no parameters, `it` is bound to the receiver. */
if (ts_node_is_null(lambda)) {
return;
}
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
const CBMType *prev_it = ctx->it_type;
/* DSL receiver: when the callee's lambda parameter has a function-with-
* receiver type (`Foo.() -> Unit`), the lambda's `this` is Foo and bare
* calls inside resolve against Foo's members. The dsl_this_type is
* passed via receiver_type when the caller can determine it. */
const CBMType *prev_this = ctx->this_type;
if (receiver_type && !cbm_type_is_unknown(receiver_type)) {
ctx->this_type = receiver_type;
}
TSNode params = kt_child_kind(lambda, "lambda_parameters");
if (ts_node_is_null(params)) {
if (receiver_type && !cbm_type_is_unknown(receiver_type)) {
ctx->it_type = receiver_type;
}
} else {
uint32_t pnc = ts_node_named_child_count(params);
for (uint32_t i = 0; i < pnc; i++) {
TSNode pn = ts_node_named_child(params, i);
TSNode id = kt_name_child(pn);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(pn, "simple_identifier");
}
if (!ts_node_is_null(id)) {
char *name = kt_node_text(ctx, id);
if (name) {
/* Type annotation? */
TSNode tn = kt_child_kind(pn, "type");
const CBMType *t =
ts_node_is_null(tn) ? cbm_type_unknown() : kotlin_parse_type_node(ctx, tn);
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, name), t);
}
}
}
}
/* Walk lambda body */
uint32_t nc = ts_node_named_child_count(lambda);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(lambda, i);
const char *k = ts_node_type(c);
if (strcmp(k, "lambda_parameters") == 0) {
continue;
}
kt_process_statement(ctx, c);
}
ctx->it_type = prev_it;
ctx->this_type = prev_this;
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
}
static void kt_process_call_with_lambda(KotlinLSPContext *ctx, TSNode call_node) {
/* If call has trailing lambda, propagate the receiver type as `it`.
* xs.forEach { println(it) } — `it` is element type of xs
* "abc".let { … } — `it` is String
*/
uint32_t nc = ts_node_named_child_count(call_node);
/* Resolve callee normally for emission */
(void)kt_eval_call_expression_type(ctx, call_node);
/* Find trailing lambda */
TSNode lambda;
memset(&lambda, 0, sizeof(lambda));
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(call_node, i);
/* Newer tree-sitter-kotlin nests the trailing lambda inside a
* `call_suffix` node; descend into it. */
if (kt_node_is(c, "call_suffix")) {
TSNode al = kt_child_kind(c, "annotated_lambda");
if (ts_node_is_null(al)) {
al = kt_child_kind(c, "lambda_literal");
}
if (ts_node_is_null(al)) {
continue;
}
c = al;
}
if (kt_node_is(c, "annotated_lambda") || kt_node_is(c, "lambda_literal")) {
lambda = c;
/* Could be wrapped in annotated_lambda */
if (kt_node_is(c, "annotated_lambda")) {
TSNode inner = kt_child_kind(c, "lambda_literal");
if (!ts_node_is_null(inner)) {
lambda = inner;
}
}
break;
}
}
if (ts_node_is_null(lambda)) {
return;
}
/* Receiver type for `it` and DSL `this`:
* - If callee is `recv.fn` and the resolved fn has a
* `lambda_receiver:` decorator hint stamped at registration, use
* that as the lambda's `this` (DSL builders).
* - Otherwise pass the navigation receiver's type as `it`.
*/
const CBMType *recv_t = NULL;
TSNode callee = ts_node_named_child(call_node, 0);
if (kt_node_is(callee, "navigation_expression")) {
TSNode rcv_node = ts_node_named_child(callee, 0);
const CBMType *outer_recv = kotlin_eval_expr_type(ctx, rcv_node);
const char *outer_qn = kt_type_qn_of(outer_recv);
if (outer_qn) {
TSNode sel = kt_nav_member_node(callee);
char *member = kt_node_text(ctx, sel);
if (member) {
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, outer_qn, member);
if (rf && rf->decorator_qns) {
for (int i = 0; rf->decorator_qns[i]; i++) {
const char *d = rf->decorator_qns[i];
if (strncmp(d, "lambda_receiver:", 16) == 0) {
recv_t = cbm_type_named(ctx->arena, d + 16);
break;
}
}
}
}
}
if (!recv_t) {
recv_t = outer_recv;
}
} else if (kt_node_is(callee, "identifier") || kt_node_is(callee, "simple_identifier")) {
/* Bare-fun call: check the resolved function's lambda receiver. */
char *fname = kt_node_text(ctx, callee);
if (fname) {
const char *fn_qn = kotlin_resolve_function_name(ctx, fname);
if (fn_qn && ctx->registry) {
const CBMRegisteredFunc *rf = cbm_registry_lookup_func(ctx->registry, fn_qn);
if (rf && rf->decorator_qns) {
for (int i = 0; rf->decorator_qns[i]; i++) {
const char *d = rf->decorator_qns[i];
if (strncmp(d, "lambda_receiver:", 16) == 0) {
recv_t = cbm_type_named(ctx->arena, d + 16);
break;
}
}
}
}
}
}
kt_process_lambda(ctx, lambda, recv_t);
}
static void kt_process_statement(KotlinLSPContext *ctx, TSNode stmt) {
if (ts_node_is_null(stmt)) {
return;
}
const char *kind = ts_node_type(stmt);
/* Newer tree-sitter-kotlin wraps a body's statements in a `statements`
* node (where older grammars used `block`/bare children). Unwrap either so
* each real statement is bound + resolved in order. */
if (strcmp(kind, "statements") == 0 || strcmp(kind, "block") == 0) {
kt_process_block_stmts(ctx, stmt);
return;
}
if (strcmp(kind, "property_declaration") == 0) {
kt_bind_property_to_scope(ctx, stmt);
/* Resolve calls within initializer */
kt_resolve_calls_in_node(ctx, stmt);
return;
}
if (strcmp(kind, "function_declaration") == 0) {
/* Local function — register and recurse */
TSNode name = kt_field_named(stmt, "name");
if (ts_node_is_null(name)) {
name = kt_child_kind_named(stmt, "simple_identifier");
}
if (ts_node_is_null(name)) {
name = kt_name_child(stmt);
}
char *fname = kt_node_text(ctx, name);
if (!fname) {
return;
}
/* Register the local fn so call sites in the enclosing scope
* can resolve it via kotlin_resolve_function_name. The QN is
* "<enclosing_func_qn>.<fname>" so it's distinct from a top-level
* function with the same short name. */
const char *prev_func = ctx->enclosing_func_qn;
const char *new_qn = kt_join_dot(ctx->arena, prev_func ? prev_func : ctx->module_qn, fname);
{
CBMRegisteredFunc lrf = {0};
lrf.qualified_name = new_qn;
lrf.short_name = fname;
lrf.min_params = 0;
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, lrf);
/* Also register a same-package alias so bare `inner()` calls
* succeed via the same-package fallback in
* kotlin_resolve_function_name. */
if (ctx->package_qn && *ctx->package_qn) {
CBMRegisteredFunc alias = lrf;
alias.qualified_name = kt_join_dot(ctx->arena, ctx->package_qn, fname);
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, alias);
} else {
/* No package — register at root. */
CBMRegisteredFunc alias = lrf;
alias.qualified_name = cbm_arena_strdup(ctx->arena, fname);
cbm_registry_add_func((CBMTypeRegistry *)ctx->registry, alias);
}
}
ctx->enclosing_func_qn = new_qn;
TSNode body = kt_field_named(stmt, "body");
if (ts_node_is_null(body)) {
body = kt_child_kind(stmt, "function_body");
}
if (ts_node_is_null(body)) {
body = kt_child_kind(stmt, "block");
}
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
kt_bind_function_params(ctx, stmt);
if (!ts_node_is_null(body)) {
kt_resolve_calls_in_node(ctx, body);
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
ctx->enclosing_func_qn = prev_func;
return;
}
if (strcmp(kind, "class_declaration") == 0 || strcmp(kind, "object_declaration") == 0) {
/* Already registered in kt_collect_top_level_decls; recurse for nested */
return;
}
if (strcmp(kind, "if_expression") == 0) {
kt_process_if_expression(ctx, stmt);
return;
}
if (strcmp(kind, "when_expression") == 0) {
kt_process_when_expression(ctx, stmt);
return;
}
if (strcmp(kind, "for_statement") == 0) {
kt_process_for_statement(ctx, stmt);
return;
}
if (strcmp(kind, "while_statement") == 0 || strcmp(kind, "do_while_statement") == 0) {
kt_resolve_calls_in_node(ctx, stmt);
return;
}
if (strcmp(kind, "try_expression") == 0) {
kt_resolve_calls_in_node(ctx, stmt);
return;
}
if (strcmp(kind, "call_expression") == 0) {
kt_process_call_with_lambda(ctx, stmt);
return;
}
if (strcmp(kind, "navigation_expression") == 0) {
kt_eval_navigation_expression_type(ctx, stmt);
return;
}
if (strcmp(kind, "block") == 0) {
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
kt_process_block_stmts(ctx, stmt);
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
return;
}
if (strcmp(kind, "assignment") == 0) {
kt_resolve_calls_in_node(ctx, stmt);
return;
}
/* Fallthrough: treat as expression — eval to populate scope, recurse for calls */
kotlin_eval_expr_type(ctx, stmt);
kt_resolve_calls_in_node(ctx, stmt);
}
/* Generic walker that fires call resolution on every call_expression in
* a subtree, *without* descending into nested function/class bodies (those
* are processed separately with their own scope). */
static void kt_resolve_calls_in_node_inner(KotlinLSPContext *ctx, TSNode node) {
if (ts_node_is_null(node)) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "function_declaration") == 0 || strcmp(kind, "class_declaration") == 0 ||
strcmp(kind, "object_declaration") == 0 || strcmp(kind, "lambda_literal") == 0 ||
strcmp(kind, "anonymous_function") == 0) {
/* Skip — handled elsewhere */
return;
}
if (strcmp(kind, "call_expression") == 0) {
kt_process_call_with_lambda(ctx, node);
} else if (strcmp(kind, "navigation_expression") == 0) {
kt_eval_navigation_expression_type(ctx, node);
} else if (strcmp(kind, "if_expression") == 0) {
kt_process_if_expression(ctx, node);
return;
} else if (strcmp(kind, "when_expression") == 0) {
kt_process_when_expression(ctx, node);
return;
} else if (strcmp(kind, "for_statement") == 0) {
kt_process_for_statement(ctx, node);
return;
} else if (strcmp(kind, "property_declaration") == 0) {
kt_bind_property_to_scope(ctx, node);
}
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
kt_resolve_calls_in_node(ctx, ts_node_child(node, i));
}
}
static void kt_bind_function_params(KotlinLSPContext *ctx, TSNode func_node) {
TSNode params = kt_field_named(func_node, "parameters");
if (ts_node_is_null(params)) {
params = kt_child_kind(func_node, "function_value_parameters");
}
if (ts_node_is_null(params)) {
return;
}
uint32_t nc = ts_node_named_child_count(params);
for (uint32_t i = 0; i < nc; i++) {
TSNode p = ts_node_named_child(params, i);
if (!kt_node_is(p, "parameter") && !kt_node_is(p, "_lambda_parameter")) {
continue;
}
TSNode name = kt_field_named(p, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(p);
}
if (ts_node_is_null(name)) {
name = kt_child_kind_named(p, "simple_identifier");
}
if (ts_node_is_null(name)) {
continue;
}
char *pname = kt_node_text(ctx, name);
if (!pname) {
continue;
}
TSNode type_node = kt_field_named(p, "type");
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(p, "type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(p, "user_type");
}
if (ts_node_is_null(type_node)) {
type_node = kt_child_kind(p, "nullable_type");
}
const CBMType *t = ts_node_is_null(type_node) ? cbm_type_unknown()
: kotlin_parse_type_node(ctx, type_node);
cbm_scope_bind(ctx->current_scope, cbm_arena_strdup(ctx->arena, pname), t);
}
/* For extension fun and method: bind `this` */
if (ctx->this_type) {
cbm_scope_bind(ctx->current_scope, "this", ctx->this_type);
}
}
/* ── top-level walking ────────────────────────────────────────────── */
static void kt_process_function_body(KotlinLSPContext *ctx, TSNode func_node, const char *func_qn,
const char *receiver_class_qn) {
const char *prev_func = ctx->enclosing_func_qn;
const char *prev_class = ctx->enclosing_class_qn;
const CBMType *prev_this = ctx->this_type;
const CBMType *prev_super = ctx->super_type;
const char *prev_super_qn = ctx->enclosing_super_qn;
ctx->enclosing_func_qn = func_qn;
const CBMRegisteredType *receiver_rt = NULL;
if (receiver_class_qn) {
ctx->this_type = cbm_type_named(ctx->arena, receiver_class_qn);
receiver_rt = cbm_registry_lookup_type(ctx->registry, receiver_class_qn);
if (receiver_rt && receiver_rt->embedded_types && receiver_rt->embedded_types[0]) {
ctx->enclosing_super_qn = receiver_rt->embedded_types[0];
ctx->super_type = cbm_type_named(ctx->arena, receiver_rt->embedded_types[0]);
}
}
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
kt_bind_function_params(ctx, func_node);
/* Bind class fields (primary-constructor val/var properties) into the
* method scope so bare references like `name.uppercase()` inside a
* method body resolve to the field's type. */
if (receiver_rt && receiver_rt->field_names && receiver_rt->field_types) {
for (int i = 0; receiver_rt->field_names[i]; i++) {
cbm_scope_bind(ctx->current_scope,
cbm_arena_strdup(ctx->arena, receiver_rt->field_names[i]),
receiver_rt->field_types[i]);
}
}
TSNode body = kt_field_named(func_node, "body");
if (ts_node_is_null(body)) {
body = kt_child_kind(func_node, "function_body");
}
if (ts_node_is_null(body)) {
body = kt_child_kind(func_node, "block");
}
/* Fallback: some grammar variants emit the single-expression body
* directly as a child of function_declaration without wrapping it in
* `function_body`. Scan the named children for the last node that
* looks like an expression or statement (i.e. not modifiers, name,
* type parameters, or value parameters). */
if (ts_node_is_null(body)) {
uint32_t nc = ts_node_named_child_count(func_node);
for (uint32_t i = nc; i > 0; i--) {
TSNode c = ts_node_named_child(func_node, i - 1);
if (ts_node_is_null(c)) {
continue;
}
const char *k = ts_node_type(c);
if (strcmp(k, "modifiers") == 0 || strcmp(k, "identifier") == 0 ||
strcmp(k, "simple_identifier") == 0 ||
strcmp(k, "function_value_parameters") == 0 || strcmp(k, "type_parameters") == 0 ||
strcmp(k, "type_constraints") == 0 || strcmp(k, "annotation") == 0) {
continue;
}
/* Skip the return-type node — the return type appears AFTER
* the parameters but is itself not the body. We identify it
* heuristically: type-shaped nodes appearing immediately
* after parameters when there's also a later body candidate.
* Since we want the LAST candidate, we accept any non-skip
* kind here — this is the body. */
body = c;
break;
}
}
if (!ts_node_is_null(body)) {
/* Three body shapes:
* 1. `block` — { ... }, walk via kt_process_block_stmts so local
* function declarations are registered before being called.
* 2. `function_body` — wrapper around either a block or an expr.
* 3. Bare expression — single-expression body.
*
* For (2) and (3) we walk every named child via
* kt_resolve_calls_in_node, which already handles call_expression /
* navigation_expression / nested constructs. We deliberately walk
* ALL children of function_body (not just the first) because the
* grammar may emit additional siblings under it (e.g. annotation
* decorations) and we want to be conservative. We also evaluate
* the body expression to populate any chained types. */
if (kt_node_is(body, "block")) {
kt_process_block_stmts(ctx, body);
} else if (kt_node_is(body, "function_body")) {
uint32_t nc = ts_node_named_child_count(body);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(body, i);
if (ts_node_is_null(c)) {
continue;
}
if (kt_node_is(c, "block") || kt_node_is(c, "statements")) {
kt_process_block_stmts(ctx, c);
} else {
kt_resolve_calls_in_node(ctx, c);
kotlin_eval_expr_type(ctx, c);
}
}
} else {
kt_resolve_calls_in_node(ctx, body);
kotlin_eval_expr_type(ctx, body);
}
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
ctx->enclosing_func_qn = prev_func;
ctx->enclosing_class_qn = prev_class;
ctx->this_type = prev_this;
ctx->super_type = prev_super;
ctx->enclosing_super_qn = prev_super_qn;
}
static void kt_walk_top_level_for_resolution(KotlinLSPContext *ctx, TSNode root,
const char *enclosing_class_qn) {
if (ts_node_is_null(root)) {
return;
}
uint32_t nc = ts_node_named_child_count(root);
if (ctx->debug) {
fprintf(stderr, "[kotlin_lsp] walk_top_level enclosing=%s root_kind=%s nc=%u\n",
enclosing_class_qn ? enclosing_class_qn : "(top)", ts_node_type(root), nc);
}
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(root, i);
const char *kind = ts_node_type(c);
if (ctx->debug) {
uint32_t sb = ts_node_start_byte(c);
uint32_t eb = ts_node_end_byte(c);
int len = (int)(eb - sb);
if (len > 50) {
len = 50;
}
fprintf(stderr, "[kotlin_lsp] child[%u]: %s '%.*s'\n", i, kind, len,
ctx->source + sb);
}
/* Recurse into ERROR nodes so partial parses (e.g. when the
* grammar can't parse `interface` at file scope) still get their
* recoverable function/class bodies resolved. */
if (strcmp(kind, "ERROR") == 0) {
kt_walk_top_level_for_resolution(ctx, c, enclosing_class_qn);
continue;
}
if (strcmp(kind, "function_declaration") == 0) {
TSNode name = kt_field_named(c, "name");
if (ts_node_is_null(name)) {
name = kt_name_child(c);
}
char *fname = kt_node_text(ctx, name);
if (!fname) {
continue;
}
/* Detect extension receiver */
TSNode receiver = kt_field_named(c, "receiver");
const char *ext_recv_qn = NULL;
if (!ts_node_is_null(receiver)) {
char *rt = kt_node_text(ctx, receiver);
if (rt) {
char *lt = strchr(rt, '<');
if (lt) {
*lt = '\0';
}
size_t rl = strlen(rt);
while (rl > 0 && rt[rl - 1] == '?') {
rt[--rl] = '\0';
}
ext_recv_qn = kotlin_resolve_class_name(ctx, rt);
}
}
char *func_qn = NULL;
if (enclosing_class_qn) {
func_qn = kt_join_dot(ctx->arena, enclosing_class_qn, fname);
} else {
func_qn = kt_join_dot(ctx->arena, ctx->package_qn, fname);
}
const char *recv_qn = enclosing_class_qn ? enclosing_class_qn : ext_recv_qn;
kt_process_function_body(ctx, c, func_qn, recv_qn);
} else if (strcmp(kind, "class_declaration") == 0 ||
strcmp(kind, "object_declaration") == 0) {
const char *cls_qn = kt_qn_for_class_decl(ctx, c);
if (!cls_qn) {
continue;
}
TSNode body = kt_child_kind(c, "class_body");
if (ts_node_is_null(body)) {
body = kt_child_kind(c, "enum_class_body");
}
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = cls_qn;
kt_walk_top_level_for_resolution(ctx, body, cls_qn);
ctx->enclosing_class_qn = prev_class;
} else if (strcmp(kind, "companion_object") == 0) {
const char *companion_qn = kt_join_dot(
ctx->arena, enclosing_class_qn ? enclosing_class_qn : ctx->package_qn, "Companion");
TSNode body = kt_child_kind(c, "class_body");
kt_walk_top_level_for_resolution(ctx, body, companion_qn);
} else if (strcmp(kind, "property_declaration") == 0) {
/* Top-level property — bind into file scope and resolve calls
* in initializer (with caller QN = property's getter QN). */
const char *prev = ctx->enclosing_func_qn;
char *prop_name = NULL;
TSNode var = kt_child_kind(c, "variable_declaration");
if (!ts_node_is_null(var)) {
TSNode id = kt_name_child(var);
if (ts_node_is_null(id)) {
id = kt_child_kind_named(var, "simple_identifier");
}
if (!ts_node_is_null(id)) {
prop_name = kt_node_text(ctx, id);
}
}
if (prop_name) {
ctx->enclosing_func_qn = kt_join_dot(
ctx->arena, enclosing_class_qn ? enclosing_class_qn : ctx->package_qn,
prop_name);
}
kt_bind_property_to_scope(ctx, c);
kt_resolve_calls_in_node(ctx, c);
ctx->enclosing_func_qn = prev;
} else if (strcmp(kind, "secondary_constructor") == 0) {
const char *prev_func = ctx->enclosing_func_qn;
ctx->enclosing_func_qn = kt_join_dot(
ctx->arena, enclosing_class_qn ? enclosing_class_qn : ctx->package_qn, "<init>");
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
kt_bind_function_params(ctx, c);
TSNode body = kt_child_kind(c, "block");
if (!ts_node_is_null(body)) {
kt_process_block_stmts(ctx, body);
}
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
ctx->enclosing_func_qn = prev_func;
} else if (strcmp(kind, "anonymous_initializer") == 0) {
const char *prev_func = ctx->enclosing_func_qn;
ctx->enclosing_func_qn = kt_join_dot(
ctx->arena, enclosing_class_qn ? enclosing_class_qn : ctx->package_qn, "<init>");
kt_resolve_calls_in_node(ctx, c);
ctx->enclosing_func_qn = prev_func;
}
}
}
/* Debug AST dumper — only active when CBM_LSP_KOTLIN_AST=1 in env. */
static void kt_debug_dump_ast(TSNode node, const char *src, int depth) {
if (depth > 8) {
return;
}
const char *kind = ts_node_type(node);
uint32_t sb = ts_node_start_byte(node);
uint32_t eb = ts_node_end_byte(node);
int len = (int)(eb - sb);
if (len > 40) {
len = 40;
}
fprintf(stderr, "%*s[%s] %.*s%s\n", depth * 2, "", kind, len, src + sb,
(int)(eb - sb) > 40 ? "…" : "");
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
kt_debug_dump_ast(ts_node_named_child(node, i), src, depth + 1);
}
}
void kotlin_lsp_process_file(KotlinLSPContext *ctx, TSNode root) {
if (ts_node_is_null(root)) {
return;
}
if (getenv("CBM_LSP_KOTLIN_AST")) {
fprintf(stderr, "=== AST for %s ===\n", ctx->rel_path ? ctx->rel_path : "<unknown>");
kt_debug_dump_ast(root, ctx->source, 0);
fprintf(stderr, "=== END AST ===\n");
}
/* 1. Package + imports */
uint32_t nc = ts_node_named_child_count(root);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(root, i);
const char *kind = ts_node_type(c);
if (strcmp(kind, "package_header") == 0) {
const char *pkg = kt_parse_package_header(ctx, c);
if (pkg && *pkg) {
ctx->package_qn = pkg;
}
} else if (strcmp(kind, "import_list") == 0) {
/* Newer tree-sitter-kotlin wraps imports in an import_list node. */
uint32_t inc = ts_node_named_child_count(c);
for (uint32_t j = 0; j < inc; j++) {
kt_parse_import_directive(ctx, ts_node_named_child(c, j));
}
} else if (strcmp(kind, "import") == 0 || strcmp(kind, "import_directive") == 0 ||
strcmp(kind, "import_header") == 0) {
kt_parse_import_directive(ctx, c);
}
}
/* 2. Collect top-level definitions for intra-file resolution. */
kt_collect_top_level_decls(ctx, root);
/* 3. Walk for call resolution. */
kt_walk_top_level_for_resolution(ctx, root, NULL);
}
static const CBMType *kt_try_smart_cast(KotlinLSPContext *ctx, TSNode call_or_nav) {
/* Currently a stub — smart-cast is applied during if/when traversal. */
(void)ctx;
(void)call_or_nav;
return NULL;
}
/* ── public entry point used by cbm.c dispatcher ──────────────────── */
/* Tree-sitter handle for re-parsing repaired sources. */
extern const TSLanguage *tree_sitter_kotlin(void);
/* Detect bodyless interface methods (`fun foo()` without `: ReturnType`
* and without `{ … }`) inside `interface X { … }` bodies — the vendored
* tree-sitter-kotlin grammar produces an `ERROR` node for these and
* loses the rest of the file. We patch the source by inserting `: Unit`
* right after the closing paren of each affected method, then re-parse
* with tree-sitter.
*
* Heuristic and intentionally permissive: walk the source byte-by-byte,
* track whether we're inside an `interface ... { ... }` block (depth
* counter on '{' / '}') and, while inside, look for `fun ` followed by
* an identifier, optional whitespace, '(', balanced parens, then check
* what follows — if it's whitespace + (',' | ';' | '\n' | '}' ) without
* an intervening ':' or '=' or '{', insert `: Unit`.
*
* Returns NULL when no patches were applied (caller keeps original
* source). Otherwise the returned arena-allocated buffer is a complete
* patched source. */
static char *kt_repair_bodyless_interface_methods(CBMArena *arena, const char *src, int src_len,
int *out_len) {
if (!src || src_len <= 0) {
return NULL;
}
/* Cheap early-out: skip when neither `interface` nor a function-type
* with a receiver (`X.() ->`) appears anywhere — those are the two
* grammar pain points this pass repairs. */
if (!strstr(src, "interface") && !strstr(src, ".() ->") && !strstr(src, ".()->")) {
return NULL;
}
/* Two-pass: first scan to find insertion points, then build.
* Each fix has an offset (insert BEFORE this src index) and an
* insertion string. The insertion strings are static const so they
* don't need to be arena-copied. */
enum {
MAX_FIXES = 64,
};
typedef struct {
int offset;
const char *text;
int text_len;
} kt_fix_t;
kt_fix_t fixes[MAX_FIXES];
int fix_count = 0;
static const char insert_unit[] = ": Unit";
static const int insert_unit_len = (int)(sizeof(insert_unit) - 1);
static const char insert_parens[] = "()";
static const int insert_parens_len = (int)(sizeof(insert_parens) - 1);
const char *p = src;
const char *end = src + src_len;
int iface_depth = 0; /* nesting of `interface ... { ... }` blocks */
int brace_depth = 0; /* total braces (so we can subtract on '}') */
int iface_brace_at[16]; /* brace_depth at which each interface opened */
int iface_stack_n = 0;
while (p < end) {
char c = *p;
/* Skip line comments */
if (c == '/' && p + 1 < end && p[1] == '/') {
while (p < end && *p != '\n') {
p++;
}
continue;
}
/* Skip block comments */
if (c == '/' && p + 1 < end && p[1] == '*') {
p += 2;
while (p + 1 < end && !(*p == '*' && p[1] == '/')) {
p++;
}
if (p + 1 < end) {
p += 2;
} else {
p = end;
}
continue;
}
/* Skip string literals */
if (c == '"') {
p++;
while (p < end && *p != '"') {
if (*p == '\\' && p + 1 < end) {
p += 2;
} else {
p++;
}
}
if (p < end) {
p++;
}
continue;
}
/* Detect `interface ` keyword at word boundary */
if (c == 'i' && p + 9 < end && strncmp(p, "interface", 9) == 0 &&
(p == src || !isalnum((unsigned char)p[-1])) &&
(isspace((unsigned char)p[9]) || p[9] == '\n')) {
/* Skip ahead to opening brace of interface body */
const char *q = p + 9;
while (q < end && *q != '{' && *q != ';' && *q != '\n') {
q++;
}
if (q < end && *q == '{') {
if (iface_stack_n < 16) {
iface_brace_at[iface_stack_n++] = brace_depth;
}
iface_depth++;
brace_depth++;
p = q + 1;
continue;
}
p = q;
continue;
}
if (c == '{') {
brace_depth++;
p++;
continue;
}
if (c == '}') {
brace_depth--;
if (iface_stack_n > 0 && iface_brace_at[iface_stack_n - 1] == brace_depth) {
iface_stack_n--;
iface_depth--;
}
p++;
continue;
}
/* Inside an interface body, detect `fun NAME(...)` followed by
* end-of-statement without a return type or body. */
if (iface_depth > 0 && c == 'f' && p + 3 < end && strncmp(p, "fun", 3) == 0 &&
(p == src || !isalnum((unsigned char)p[-1])) &&
(isspace((unsigned char)p[3]) || p[3] == '\n')) {
const char *q = p + 3;
while (q < end && (isspace((unsigned char)*q) || *q == '\n')) {
q++;
}
/* Identifier */
while (q < end && (isalnum((unsigned char)*q) || *q == '_' || *q == '`')) {
q++;
}
/* Optional generic type parameters */
if (q < end && *q == '<') {
int g = 1;
q++;
while (q < end && g > 0) {
if (*q == '<') {
g++;
} else if (*q == '>') {
g--;
}
q++;
}
}
/* Optional whitespace, then '(' */
while (q < end && (isspace((unsigned char)*q) || *q == '\n')) {
q++;
}
if (q >= end || *q != '(') {
p++;
continue;
}
int paren = 1;
q++;
while (q < end && paren > 0) {
if (*q == '(') {
paren++;
} else if (*q == ')') {
paren--;
}
q++;
}
/* q is now positioned just past the ')'. */
int paren_close = (int)(q - src) - 1;
/* Skip whitespace */
const char *r = q;
while (r < end && (*r == ' ' || *r == '\t')) {
r++;
}
/* Check what follows */
if (r < end && (*r == ':' || *r == '=' || *r == '{')) {
/* Has return type, body, or expression — fine. */
p = r;
continue;
}
/* Bodyless / no return type — insert `: Unit` after the ')'. */
if (fix_count < MAX_FIXES) {
fixes[fix_count].offset = paren_close + 1;
fixes[fix_count].text = insert_unit;
fixes[fix_count].text_len = insert_unit_len;
fix_count++;
}
p = r;
continue;
}
p++;
}
/* Second pass: strip the receiver-with-dot from `<Type>.() -> X`
* function-type patterns. The vendored grammar can't parse the
* receiver-style function type at file scope, so removing the
* `<Type>.` prefix preserves parseability while losing only the
* receiver-binding hint (which we recover separately via
* decorator_qns at registration time). */
typedef struct {
int start; /* inclusive */
int end; /* exclusive */
} cut_range_t;
enum { MAX_CUTS = 64 };
cut_range_t cuts[MAX_CUTS];
int cut_count = 0;
{
const char *p2 = src;
const char *end2 = src + src_len;
while (p2 < end2 - 4) {
/* Skip strings/comments to keep things simple */
if (*p2 == '"') {
p2++;
while (p2 < end2 && *p2 != '"') {
if (*p2 == '\\' && p2 + 1 < end2) {
p2 += 2;
} else {
p2++;
}
}
if (p2 < end2) {
p2++;
}
continue;
}
if (*p2 == '/' && p2 + 1 < end2 && p2[1] == '/') {
while (p2 < end2 && *p2 != '\n') {
p2++;
}
continue;
}
if (*p2 == '/' && p2 + 1 < end2 && p2[1] == '*') {
p2 += 2;
while (p2 + 1 < end2 && !(*p2 == '*' && p2[1] == '/')) {
p2++;
}
p2 += (p2 + 1 < end2 ? 2 : 1);
continue;
}
/* Look for `.()` */
if (*p2 == '.' && p2 + 2 < end2 && p2[1] == '(' && p2[2] == ')') {
/* Walk backwards to find the identifier preceding '.' */
const char *q = p2 - 1;
while (q > src && (*q == ' ' || *q == '\t')) {
q--;
}
/* q now points at the last char of the identifier (or
* a non-identifier character). */
const char *id_end = q + 1;
while (q >= src && (isalnum((unsigned char)*q) || *q == '_' || *q == '`')) {
q--;
}
const char *id_start = q + 1;
if (id_start >= id_end) {
p2++;
continue;
}
/* Look ahead past `.()` for ` -> ` or `->` to confirm
* this is a function-type receiver and not a method
* reference. */
const char *r = p2 + 3;
while (r < end2 && (*r == ' ' || *r == '\t')) {
r++;
}
if (r + 1 >= end2 || r[0] != '-' || r[1] != '>') {
p2++;
continue;
}
/* Cut range: from id_start to (p2+1), removing the
* "<Name>." prefix and leaving "()". */
if (cut_count < MAX_CUTS) {
cuts[cut_count].start = (int)(id_start - src);
cuts[cut_count].end = (int)(p2 + 1 - src);
cut_count++;
}
p2 += 3;
continue;
}
p2++;
}
}
/* Third pass: add `()` after `: <Type>` in class delegation when
* the next token is `{` (interface inheritance). The vendored
* tree-sitter-kotlin grammar treats `class X : Y { ... }` as a
* parse error and produces an ERROR node swallowing the rest of
* the file; rewriting to `class X : Y() { ... }` keeps the parse
* intact (even though, for interface inheritance, parens are
* non-idiomatic Kotlin). We only insert when no parens already
* follow the type. */
{
const char *p3 = src;
const char *end3 = src + src_len;
while (p3 < end3) {
if (*p3 == '"') {
p3++;
while (p3 < end3 && *p3 != '"') {
if (*p3 == '\\' && p3 + 1 < end3) {
p3 += 2;
} else {
p3++;
}
}
if (p3 < end3) {
p3++;
}
continue;
}
if (*p3 == '/' && p3 + 1 < end3 && p3[1] == '/') {
while (p3 < end3 && *p3 != '\n') {
p3++;
}
continue;
}
if (*p3 == '/' && p3 + 1 < end3 && p3[1] == '*') {
p3 += 2;
while (p3 + 1 < end3 && !(*p3 == '*' && p3[1] == '/')) {
p3++;
}
p3 += (p3 + 1 < end3 ? 2 : 1);
continue;
}
if (*p3 == ':' && p3 + 1 < end3 && (p3[1] == ' ' || p3[1] == '\t' || p3[1] == '\n')) {
/* Walk back: ensure preceded by `class IDENT[(params)]?`. */
/* Walk forward: skip ws, read identifier (possibly dotted),
* skip generics, then check for parens or `{`. */
const char *q = p3 + 1;
while (q < end3 && (*q == ' ' || *q == '\t' || *q == '\n')) {
q++;
}
/* dotted identifier */
while (q < end3 && (isalnum((unsigned char)*q) || *q == '_' || *q == '.')) {
q++;
}
/* generics? */
if (q < end3 && *q == '<') {
int g = 1;
q++;
while (q < end3 && g > 0) {
if (*q == '<') {
g++;
} else if (*q == '>') {
g--;
}
q++;
}
}
/* Skip ws */
while (q < end3 && (*q == ' ' || *q == '\t' || *q == '\n')) {
q++;
}
if (q >= end3) {
p3++;
continue;
}
if (*q == '(') {
/* Already has parens. */
p3 = q;
continue;
}
if (*q != '{') {
p3++;
continue;
}
/* Walk back to verify this is class inheritance: look for
* "class " keyword on the same logical line before `:`. */
const char *back = p3 - 1;
while (back > src && *back != '\n' && back > p3 - 200) {
back--;
}
if (back <= src || back < p3 - 200) {
p3++;
continue;
}
if (!strstr(back, "class ") && !strstr(back, "object ")) {
p3++;
continue;
}
/* Insert `()` right before `{` (which is at position q). */
if (fix_count < MAX_FIXES) {
fixes[fix_count].offset = (int)(q - src);
fixes[fix_count].text = insert_parens;
fixes[fix_count].text_len = insert_parens_len;
fix_count++;
}
p3 = q;
continue;
}
p3++;
}
}
if (fix_count == 0 && cut_count == 0) {
return NULL;
}
/* Sort cuts by start offset (small N — bubble sort is fine). */
for (int i = 0; i < cut_count - 1; i++) {
for (int j = i + 1; j < cut_count; j++) {
if (cuts[j].start < cuts[i].start) {
cut_range_t tmp = cuts[i];
cuts[i] = cuts[j];
cuts[j] = tmp;
}
}
}
/* Sort fixes by offset (different passes may interleave). */
for (int i = 0; i < fix_count - 1; i++) {
for (int j = i + 1; j < fix_count; j++) {
if (fixes[j].offset < fixes[i].offset) {
kt_fix_t tmp = fixes[i];
fixes[i] = fixes[j];
fixes[j] = tmp;
}
}
}
/* Build patched source. Cuts remove a [start, end) range. */
int new_len = src_len;
for (int i = 0; i < fix_count; i++) {
new_len += fixes[i].text_len;
}
for (int i = 0; i < cut_count; i++) {
new_len -= (cuts[i].end - cuts[i].start);
}
char *out = (char *)cbm_arena_alloc(arena, (size_t)new_len + 1);
if (!out) {
return NULL;
}
int oi = 0;
int next_fix = 0;
int next_cut = 0;
for (int i = 0; i <= src_len; i++) {
while (next_fix < fix_count && fixes[next_fix].offset == i) {
memcpy(out + oi, fixes[next_fix].text, (size_t)fixes[next_fix].text_len);
oi += fixes[next_fix].text_len;
next_fix++;
}
/* Skip range [start, end) when cutting */
if (next_cut < cut_count && cuts[next_cut].start == i) {
i = cuts[next_cut].end - 1; /* loop's i++ moves past `end-1` */
next_cut++;
continue;
}
if (i < src_len) {
out[oi++] = src[i];
}
}
out[oi] = '\0';
if (out_len) {
*out_len = oi;
}
return out;
}
void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root) {
if (!arena || !result || !source || ts_node_is_null(root)) {
return;
}
/* Repair pass: if the source uses interface declarations with
* bodyless `fun X()` methods or function-types with receivers
* (`Foo.() -> X`), our vendored tree-sitter grammar produces
* ERROR nodes and loses parts of the file. Patch the source and
* re-parse. */
int patched_len = 0;
char *patched_src =
kt_repair_bodyless_interface_methods(arena, source, source_len, &patched_len);
TSTree *patched_tree = NULL;
TSNode use_root = root;
const char *use_source = source;
int use_source_len = source_len;
bool debug = (getenv("CBM_LSP_DEBUG") != NULL);
if (debug && patched_src) {
fprintf(stderr, "[kotlin_lsp] preprocessed %d → %d bytes\n", source_len, patched_len);
fprintf(stderr, "[kotlin_lsp] patched source:\n%s\n[end patched]\n", patched_src);
} else if (debug) {
fprintf(stderr, "[kotlin_lsp] no preprocessing applied (no interface or .() patterns)\n");
}
if (patched_src) {
TSParser *parser = ts_parser_new();
if (parser) {
const TSLanguage *lang = tree_sitter_kotlin();
if (debug) {
fprintf(stderr, "[kotlin_lsp] tree_sitter_kotlin lang=%p\n", (void *)lang);
}
ts_parser_set_language(parser, lang);
patched_tree = ts_parser_parse_string(parser, NULL, patched_src, (uint32_t)patched_len);
ts_parser_delete(parser);
if (debug) {
fprintf(stderr, "[kotlin_lsp] re-parse result tree=%p\n", (void *)patched_tree);
}
if (patched_tree) {
use_root = ts_tree_root_node(patched_tree);
use_source = patched_src;
use_source_len = patched_len;
}
}
}
/* Build per-file registry. */
CBMTypeRegistry registry;
cbm_registry_init(&registry, arena);
/* Curated stdlib */
cbm_kotlin_stdlib_register(&registry, arena);
/* Compute project name + package_qn from result->module_qn (which is
* "<project>.<rel.path.parts>"). The Kotlin convention places the
* file class as "<project>.<package>" — or "<project>.<rel-path>" for
* the module_qn. We honour module_qn as-is and strip the trailing
* filename to derive the package_qn at the FS-path level — but for
* cross-file resolution we additionally need the dotted package
* declared in the source. The kotlin_lsp_process_file pass updates
* package_qn from the actual `package_header` node when present.
*/
const char *project_name = "";
const char *module_qn = result->module_qn ? result->module_qn : "";
const char *first_dot = strchr(module_qn, '.');
if (first_dot) {
size_t pl = (size_t)(first_dot - module_qn);
char *pn = (char *)cbm_arena_alloc(arena, pl + 1);
if (pn) {
memcpy(pn, module_qn, pl);
pn[pl] = '\0';
project_name = pn;
}
} else {
project_name = module_qn;
}
/* Initial package_qn is the FS-path module_qn ("<project>.<rel.path>"),
* matching the textual extractor's QN prefix so the LSP's caller_qn equals
* the call site's enclosing_func_qn (the join keys on an exact caller_qn
* match). A source `package_header`, when present, overrides this in
* kotlin_lsp_process_file for cross-file import resolution. */
KotlinLSPContext ctx;
kotlin_lsp_init(&ctx, arena, use_source, use_source_len, &registry, module_qn, module_qn,
project_name, /*rel_path=*/NULL, &result->resolved_calls);
kotlin_lsp_process_file(&ctx, use_root);
/* Inject kotlin.Any universal-method nodes (toString/equals/hashCode) so the
* lsp_kt_any fallback emitted above has a target node to form a CALLS edge. */
kt_builtins_inject_defs(result, arena);
if (patched_tree) {
ts_tree_delete(patched_tree);
}
}
/* ── Cross-file LSP ───────────────────────────────────────────────── */
/* Register one cross-file definition into the registry under its graph QN so
* a call site in another file resolves to the right node. Types and functions
* keep their full project-qualified QN; functions carry receiver_type so the
* sole-definer fallback can tell a top-level fun from a method. */
static const char *kt_cross_builtin_return_qn(const char *name) {
if (!name) {
return NULL;
}
if (strcmp(name, "String") == 0) {
return "kotlin.String";
}
if (strcmp(name, "Int") == 0 || strcmp(name, "Integer") == 0) {
return "kotlin.Int";
}
if (strcmp(name, "Long") == 0) {
return "kotlin.Long";
}
if (strcmp(name, "Float") == 0) {
return "kotlin.Float";
}
if (strcmp(name, "Double") == 0) {
return "kotlin.Double";
}
if (strcmp(name, "Boolean") == 0 || strcmp(name, "Bool") == 0) {
return "kotlin.Boolean";
}
if (strcmp(name, "Char") == 0 || strcmp(name, "Character") == 0) {
return "kotlin.Char";
}
if (strcmp(name, "Byte") == 0) {
return "kotlin.Byte";
}
if (strcmp(name, "Short") == 0) {
return "kotlin.Short";
}
if (strcmp(name, "Unit") == 0 || strcmp(name, "Void") == 0 || strcmp(name, "void") == 0) {
return "kotlin.Unit";
}
if (strcmp(name, "Any") == 0 || strcmp(name, "Object") == 0) {
return "kotlin.Any";
}
return NULL;
}
static const CBMType *kt_cross_return_type(CBMArena *arena, const CBMLSPDef *d) {
if (!arena || !d || !d->return_types || !d->return_types[0]) {
return NULL;
}
const char *text = d->return_types;
const char *bar = strchr(text, '|');
const char *first = bar ? cbm_arena_strndup(arena, text, (size_t)(bar - text)) : text;
if (!first || !first[0]) {
return NULL;
}
if (!strchr(first, '.')) {
const char *builtin = kt_cross_builtin_return_qn(first);
if (builtin) {
first = builtin;
} else if (d->namespace_name && d->namespace_name[0]) {
first = kt_join_dot(arena, d->namespace_name, first);
}
}
return cbm_type_named(arena, first);
}
static const CBMType *kt_cross_func_sig_with_return(CBMArena *arena, const CBMLSPDef *d) {
const CBMType *ret = kt_cross_return_type(arena, d);
if (!ret || cbm_type_is_unknown(ret)) {
return NULL;
}
const char **empty_pn = (const char **)cbm_arena_alloc(arena, sizeof(*empty_pn));
const CBMType **empty_pt = (const CBMType **)cbm_arena_alloc(arena, sizeof(*empty_pt));
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
if (!empty_pn || !empty_pt || !rets) {
return NULL;
}
empty_pn[0] = NULL;
empty_pt[0] = NULL;
rets[0] = ret;
rets[1] = NULL;
return cbm_type_func(arena, empty_pn, empty_pt, rets);
}
static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const CBMLSPDef *d) {
if (!d->qualified_name || !d->short_name || !d->label) {
return;
}
if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 ||
strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name;
rt.short_name = d->short_name;
rt.is_interface = (strcmp(d->label, "Interface") == 0) || d->is_interface;
if (d->embedded_types && d->embedded_types[0]) {
int n = 1;
for (const char *p = d->embedded_types; *p; p++) {
if (*p == '|') {
n++;
}
}
const char **emb =
(const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*emb));
int idx = 0;
const char *start = d->embedded_types;
while (*start) {
const char *end = start;
while (*end && *end != '|') {
end++;
}
if (end > start) {
emb[idx++] = cbm_arena_strndup(arena, start, (size_t)(end - start));
}
if (!*end) {
break;
}
start = end + 1;
}
emb[idx] = NULL;
rt.embedded_types = emb;
}
cbm_registry_add_type(reg, rt);
} else if (strcmp(d->label, "Method") == 0 || strcmp(d->label, "Function") == 0 ||
strcmp(d->label, "Constructor") == 0) {
CBMRegisteredFunc rf;
memset(&rf, 0, sizeof(rf));
rf.qualified_name = d->qualified_name;
rf.short_name = d->short_name;
rf.min_params = -1;
/* receiver_type distinguishes a top-level fun (NULL) from a method
* (set) — the sole-definer fallback only matches top-level funs. */
rf.receiver_type = d->receiver_type;
rf.signature = kt_cross_func_sig_with_return(arena, d);
cbm_registry_add_func(reg, rf);
}
}
void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!arena || !source) {
return;
}
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
cbm_kotlin_stdlib_register(&reg, arena);
/* Register project-wide defs (local + cross-file) under their graph QNs. */
for (int i = 0; i < def_count; i++) {
kt_register_cross_def(&reg, arena, &defs[i]);
}
/* Build the hash indexes: without this every registry lookup in the walk
* is a LINEAR scan over the whole cross registry — O(lookups x defs) per
* file (same class as the java_lsp/elasticsearch slowdown). Indexes live
* in a per-call scratch arena (reg's arena is pipeline-lifetime). */
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
cbm_registry_finalize_into(&reg, &idx_arena);
/* Parse the source if the pipeline didn't hand us a cached tree. */
TSTree *tree = cached_tree;
bool owns_tree = false;
if (!tree) {
TSParser *parser = ts_parser_new();
if (!parser) {
cbm_arena_destroy(&idx_arena);
return;
}
ts_parser_set_language(parser, tree_sitter_kotlin());
tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len);
ts_parser_delete(parser);
owns_tree = true;
}
if (!tree) {
cbm_arena_destroy(&idx_arena);
return;
}
TSNode root = ts_tree_root_node(tree);
/* project_name prefix (everything before the first dot of module_qn). */
const char *project_name = "";
const char *first_dot = module_qn ? strchr(module_qn, '.') : NULL;
if (first_dot) {
size_t pl = (size_t)(first_dot - module_qn);
char *pn = (char *)cbm_arena_alloc(arena, pl + 1);
if (pn) {
memcpy(pn, module_qn, pl);
pn[pl] = '\0';
project_name = pn;
}
} else if (module_qn) {
project_name = module_qn;
}
KotlinLSPContext ctx;
kotlin_lsp_init(&ctx, arena, source, source_len, &reg, "", 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++) {
if (!import_names || !import_qns || !import_names[i] || !import_qns[i]) {
continue;
}
kotlin_lsp_add_import(&ctx, import_names[i], import_qns[i], CBM_KT_USE_UNKNOWN);
}
kotlin_lsp_process_file(&ctx, root);
cbm_arena_destroy(&idx_arena);
if (owns_tree) {
ts_tree_delete(tree);
}
}