/* * py_lsp.c — Python type-aware call resolution. * * Implements the Python LSP-style binder + type evaluator + call resolver. * Mirrors go_lsp.c / c_lsp.c: * - py_lsp_bind_imports (Phase 3) — imports → root scope * - py_eval_expr_type (Phase 5) — single-expression type * - py_process_statement (Phase 4) — assignment / for / with binding * - py_lookup_attribute (Phase 6) — attribute walk with MRO * - py_resolve_calls_in (Phase 4-6) — recursive AST walker emitting * resolved_calls entries */ #include "py_lsp.h" #include "../cbm.h" #include "../helpers.h" #include "tree_sitter/api.h" #include #include #include /* Minimal Python builtins as real graph nodes (py_builtins_inject_defs). * #included here (CGo amalgamation pattern, see lsp_all.c) — referenced * only from py_lsp.c, never compiled standalone. */ #include "py_builtins.c" /* Guards for py_eval_expr_type — mirrors c_eval_expr_type's guard design * (C_EVAL_DEPTH_LIMIT / C_EVAL_MAX_STEPS_PER_FILE in c_lsp.c). */ #define PY_LSP_MAX_EVAL_DEPTH 256 #define PY_EVAL_MAX_STEPS_PER_FILE 10000 // Forward decls static void py_resolve_calls_in_inner(PyLSPContext *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 py_resolve_calls_in(PyLSPContext *ctx, TSNode node) { if (ctx->walk_depth >= cbm_lsp_max_walk_depth()) return; ctx->walk_depth++; py_resolve_calls_in_inner(ctx, node); ctx->walk_depth--; } static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node); static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node); static void py_process_statement(PyLSPContext *ctx, TSNode node); static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn, const char *member_name); static void py_emit_resolved_call(PyLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence); static const CBMType *py_resolve_annotation(PyLSPContext *ctx, const char *ann); static const CBMType *py_iterable_element_type(PyLSPContext *ctx, const CBMType *iter_type); static const CBMType *py_lookup_field(PyLSPContext *ctx, const char *type_qn, const char *field_name); static void py_register_instance_field(PyLSPContext *ctx, const char *class_qn, const char *field_name, const CBMType *field_type); static void py_bind_for_target(PyLSPContext *ctx, TSNode left, const CBMType *elem_type); static void py_register_lambda(PyLSPContext *ctx, const char *name, TSNode lambda_node); static void py_register_dict_literal(PyLSPContext *ctx, const char *name, TSNode dict_node); static TSNode py_lookup_lambda(PyLSPContext *ctx, const char *name); static const char *py_lookup_dict_dispatch(PyLSPContext *ctx, const char *var, const char *key); /* ── Scope mutation wrappers ───────────────────────────────────────────── * * py_eval_expr_type memoizes per-node results (see the cache block above * py_eval_expr_type). A cached result is only valid as long as name lookup * resolves exactly as it did when the entry was computed, so every mutation * of the scope chain must invalidate the cache. Bumping the generation * counter is an O(1) whole-cache flush: entries from older generations * never match again. All binds/restores in this file MUST go through these * wrappers — never call cbm_scope_bind(ctx->current_scope, ...) or assign * ctx->current_scope directly (scope pushes are exempt: an empty child * scope delegates every lookup to its parent, changing nothing). * * This is what makes memoization behavior-preserving even when the same * node is legitimately re-evaluated under different bindings — e.g. a * lambda body re-walked per call site with per-call argument types, or * isinstance-narrowed branches. */ static void py_scope_bind(PyLSPContext *ctx, const char *name, const CBMType *type) { ctx->type_cache_gen++; cbm_scope_bind(ctx->current_scope, name, type); } static void py_scope_restore(PyLSPContext *ctx, CBMScope *saved) { ctx->type_cache_gen++; ctx->current_scope = saved; } void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int source_len, const CBMTypeRegistry *registry, const char *module_qn, CBMResolvedCallArray *out) { if (!ctx) return; memset(ctx, 0, sizeof(PyLSPContext)); ctx->arena = arena; ctx->source = source; ctx->source_len = source_len; ctx->registry = registry; ctx->module_qn = module_qn; ctx->resolved_calls = out; ctx->current_scope = cbm_scope_push(arena, NULL); const char *dbg = getenv("CBM_LSP_DEBUG"); ctx->debug = dbg && dbg[0] && dbg[0] != '0'; } void py_lsp_add_import(PyLSPContext *ctx, const char *local_name, const char *module_qn) { if (!ctx || !local_name || !module_qn) return; int new_count = ctx->import_count + 1; const char **names = (const char **)cbm_arena_alloc(ctx->arena, (size_t)(new_count + 1) * sizeof(const char *)); const char **qns = (const char **)cbm_arena_alloc(ctx->arena, (size_t)(new_count + 1) * sizeof(const char *)); if (!names || !qns) return; for (int i = 0; i < ctx->import_count; i++) { names[i] = ctx->import_local_names[i]; qns[i] = ctx->import_module_qns[i]; } names[ctx->import_count] = cbm_arena_strdup(ctx->arena, local_name); qns[ctx->import_count] = cbm_arena_strdup(ctx->arena, module_qn); names[new_count] = NULL; qns[new_count] = NULL; ctx->import_local_names = names; ctx->import_module_qns = qns; ctx->import_count = new_count; } /* Determine whether this import is an `import X` style binding (binds the * module itself) or a `from X import Y` style binding (binds Y, an * attribute of module X). Heuristic: if module_qn ends in `.` * and has at least two components, it's a from-import; otherwise it's a * straight module import. Matches the shape produced by extract_imports.c. */ static bool import_is_from_style(const char *local_name, const char *module_qn) { if (!local_name || !module_qn) return false; size_t local_len = strlen(local_name); size_t mod_len = strlen(module_qn); if (mod_len <= local_len) return false; if (module_qn[mod_len - local_len - 1] != '.') return false; if (strcmp(module_qn + mod_len - local_len, local_name) != 0) return false; return true; } /* For `import a.b.c`, also bind every dotted prefix as MODULE so that * `a.b.c.fn()` style chained access walks correctly: `a` → MODULE(a), * `a.b` → MODULE(a.b), `a.b.c` → MODULE(a.b.c). The underlying CBMImport * already records local_name="c" / module_path="a.b.c"; we walk the * prefix chain in addition. */ static void py_bind_dotted_prefixes(PyLSPContext *ctx, const char *qn) { if (!ctx || !qn) return; const char *p = qn; for (;;) { const char *dot = strchr(p, '.'); if (!dot) break; size_t prefix_len = (size_t)(dot - qn); char *prefix = (char *)cbm_arena_alloc(ctx->arena, prefix_len + 1); if (!prefix) return; memcpy(prefix, qn, prefix_len); prefix[prefix_len] = '\0'; // Also bind the *short top-level name* — for `import a.b.c`, // the source typically writes `a.b.c.fn()` and `a` must be in // scope as MODULE("a") (not the full QN, since attribute access // walks one segment at a time). const char *short_name = strrchr(prefix, '.'); const char *bind_short = short_name ? short_name + 1 : prefix; if (cbm_type_is_unknown(cbm_scope_lookup(ctx->current_scope, bind_short))) { py_scope_bind(ctx, bind_short, cbm_type_module(ctx->arena, prefix)); } p = dot + 1; } } void py_lsp_bind_imports(PyLSPContext *ctx) { if (!ctx || !ctx->current_scope) return; for (int i = 0; i < ctx->import_count; i++) { const char *local = ctx->import_local_names[i]; const char *qn = ctx->import_module_qns[i]; if (!local || !qn) continue; // Wildcard imports are recorded for traceability but cannot bind a // concrete name — skip the scope insertion. Phase 9 cross-file // logic will use the import map directly to find re-exports. if (strcmp(local, "*") == 0) continue; const CBMType *t; if (import_is_from_style(local, qn)) { // `from X import Y` — bind Y to NAMED(X.Y). Phase 6 attribute // resolution checks the registry to upgrade to MODULE / class // / function as appropriate. t = cbm_type_named(ctx->arena, qn); } else { // `import X` / `import X as Y` — bind to MODULE(X). t = cbm_type_module(ctx->arena, qn); } py_scope_bind(ctx, local, t); // Always walk the dotted prefix chain. The CBMImport shape // can't distinguish `import a.b.c` from `from a.b import c` // (both produce local_name=c, module_path=a.b.c), but binding // parent modules (`a`, `a.b`) into scope is correct in both // cases: in the first form Python actually does this; in the // second form the parent isn't in scope at runtime, but our // adding it doesn't cause false positives because real source // wouldn't reference an unimported parent module name. py_bind_dotted_prefixes(ctx, qn); } } const CBMType *py_lsp_lookup_in_scope(const PyLSPContext *ctx, const char *name) { if (!ctx) return cbm_type_unknown(); return cbm_scope_lookup(ctx->current_scope, name); } /* ── helpers: text + emit ─────────────────────────────────────── */ static char *py_node_text(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return NULL; uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); if (end <= start || (int)end > ctx->source_len) return NULL; size_t len = (size_t)(end - start); char *out = (char *)cbm_arena_alloc(ctx->arena, len + 1); if (!out) return NULL; memcpy(out, ctx->source + start, len); out[len] = '\0'; return out; } /* ── lambda + dict-literal-dispatch registries ────────────────── */ static void py_register_lambda(PyLSPContext *ctx, const char *name, TSNode lambda_node) { if (!ctx || !name) return; if (ctx->lambda_count >= ctx->lambda_cap) { int new_cap = ctx->lambda_cap == 0 ? 8 : ctx->lambda_cap * 2; CBMLambdaEntry *grown = (CBMLambdaEntry *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(CBMLambdaEntry)); if (!grown) return; if (ctx->lambdas && ctx->lambda_count > 0) { memcpy(grown, ctx->lambdas, (size_t)ctx->lambda_count * sizeof(CBMLambdaEntry)); } ctx->lambdas = grown; ctx->lambda_cap = new_cap; } // Overwrite if name already registered. for (int i = 0; i < ctx->lambda_count; i++) { if (ctx->lambdas[i].name && strcmp(ctx->lambdas[i].name, name) == 0) { ctx->lambdas[i].lambda_node = lambda_node; return; } } ctx->lambdas[ctx->lambda_count].name = cbm_arena_strdup(ctx->arena, name); ctx->lambdas[ctx->lambda_count].lambda_node = lambda_node; ctx->lambda_count++; } static TSNode py_lookup_lambda(PyLSPContext *ctx, const char *name) { TSNode null_node = {0}; if (!ctx || !name) return null_node; for (int i = 0; i < ctx->lambda_count; i++) { if (ctx->lambdas[i].name && strcmp(ctx->lambdas[i].name, name) == 0) { return ctx->lambdas[i].lambda_node; } } return null_node; } /* Helper for stripping quotes from a tree-sitter `string` node text. */ static char *py_string_literal_value(PyLSPContext *ctx, TSNode str_node) { if (ts_node_is_null(str_node)) return NULL; char *lit = py_node_text(ctx, str_node); if (!lit) return NULL; size_t len = strlen(lit); if (len >= 2 && (lit[0] == '"' || lit[0] == '\'') && lit[len - 1] == lit[0]) { char *out = (char *)cbm_arena_alloc(ctx->arena, len - 1); if (!out) return NULL; memcpy(out, lit + 1, len - 2); out[len - 2] = '\0'; return out; } return lit; } static void py_register_dict_literal(PyLSPContext *ctx, const char *var, TSNode dict_node) { if (!ctx || !var || ts_node_is_null(dict_node)) return; uint32_t nc = ts_node_named_child_count(dict_node); for (uint32_t i = 0; i < nc; i++) { TSNode pair = ts_node_named_child(dict_node, i); if (strcmp(ts_node_type(pair), "pair") != 0) continue; TSNode key = ts_node_child_by_field_name(pair, "key", 3); TSNode value = ts_node_child_by_field_name(pair, "value", 5); if (ts_node_is_null(key) || ts_node_is_null(value)) continue; if (strcmp(ts_node_type(key), "string") != 0) continue; if (strcmp(ts_node_type(value), "identifier") != 0) continue; char *key_text = py_string_literal_value(ctx, key); char *val_text = py_node_text(ctx, value); if (!key_text || !val_text) continue; // Resolve val_text to a registered function QN. const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, val_text); if (!f) { f = cbm_registry_lookup_symbol(ctx->registry, "builtins", val_text); } if (!f) continue; if (ctx->dict_literal_count >= ctx->dict_literal_cap) { int new_cap = ctx->dict_literal_cap == 0 ? 16 : ctx->dict_literal_cap * 2; CBMDictLiteralEntry *grown = (CBMDictLiteralEntry *)cbm_arena_alloc( ctx->arena, (size_t)new_cap * sizeof(CBMDictLiteralEntry)); if (!grown) return; if (ctx->dict_literals && ctx->dict_literal_count > 0) { memcpy(grown, ctx->dict_literals, (size_t)ctx->dict_literal_count * sizeof(CBMDictLiteralEntry)); } ctx->dict_literals = grown; ctx->dict_literal_cap = new_cap; } ctx->dict_literals[ctx->dict_literal_count].var_name = cbm_arena_strdup(ctx->arena, var); ctx->dict_literals[ctx->dict_literal_count].literal_key = cbm_arena_strdup(ctx->arena, key_text); ctx->dict_literals[ctx->dict_literal_count].target_qn = cbm_arena_strdup(ctx->arena, f->qualified_name); ctx->dict_literal_count++; } } static const char *py_lookup_dict_dispatch(PyLSPContext *ctx, const char *var, const char *key) { if (!ctx || !var || !key) return NULL; for (int i = 0; i < ctx->dict_literal_count; i++) { const CBMDictLiteralEntry *e = &ctx->dict_literals[i]; if (e->var_name && e->literal_key && strcmp(e->var_name, var) == 0 && strcmp(e->literal_key, key) == 0) { return e->target_qn; } } return NULL; } static void py_emit_resolved_call_reason(PyLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence, const char *reason) { if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return; // Dedupe by (caller, callee). Bounded-window scan: most duplicate // emissions are nearby in time (same expression evaluated by both // resolver and emitter passes), so checking only the last DEDUP_WINDOW // entries catches the common case while keeping per-emission O(1). // Without this cap the dedup is O(N) per emission -> O(N^2) per file // and dominates above ~1k call sites. enum { DEDUP_WINDOW = 256 }; int n = ctx->resolved_calls->count; int start = n > DEDUP_WINDOW ? n - DEDUP_WINDOW : 0; for (int i = start; i < n; i++) { CBMResolvedCall *rc = &ctx->resolved_calls->items[i]; if (rc->caller_qn && rc->callee_qn && strcmp(rc->caller_qn, ctx->enclosing_func_qn) == 0 && strcmp(rc->callee_qn, callee_qn) == 0) { if (confidence > rc->confidence) { rc->confidence = confidence; rc->strategy = strategy; } return; } } CBMResolvedCall rc; memset(&rc, 0, sizeof(rc)); rc.caller_qn = ctx->enclosing_func_qn; rc.callee_qn = cbm_arena_strdup(ctx->arena, callee_qn); rc.strategy = strategy; rc.confidence = confidence; rc.reason = reason ? cbm_arena_strdup(ctx->arena, reason) : NULL; cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } static void py_emit_resolved_call(PyLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence) { py_emit_resolved_call_reason(ctx, callee_qn, strategy, confidence, NULL); } /* ── helpers: registry-driven attribute lookup with depth cap ──── */ static const CBMRegisteredFunc *py_lookup_attribute_depth(PyLSPContext *ctx, const char *type_qn, const char *member_name, int depth) { if (!ctx || !type_qn || !member_name) return NULL; if (depth > CBM_LSP_MAX_LOOKUP_DEPTH) return NULL; const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name); if (f) return f; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn); if (rt) { if (rt->alias_of) { f = py_lookup_attribute_depth(ctx, rt->alias_of, member_name, depth + 1); if (f) return f; } if (rt->embedded_types) { for (int i = 0; rt->embedded_types[i]; i++) { f = py_lookup_attribute_depth(ctx, rt->embedded_types[i], member_name, depth + 1); if (f) return f; } } } return NULL; } static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn, const char *member_name) { return py_lookup_attribute_depth(ctx, type_qn, member_name, 0); } /* Per-file field overlay: look up a (class_qn, field_name) recorded during resolve * against the sealed shared registry. Linear scan — the overlay only holds fields * discovered in THIS file's own classes, so it is small. */ static const CBMType *py_overlay_lookup_field(const PyLSPContext *ctx, const char *class_qn, const char *field_name) { for (int i = 0; i < ctx->field_overlay_count; i++) { if (strcmp(ctx->field_overlay[i].class_qn, class_qn) == 0 && strcmp(ctx->field_overlay[i].field_name, field_name) == 0) return ctx->field_overlay[i].field_type; } return NULL; } /* Record a (class_qn, field_name) -> field_type in the per-file overlay. Overwrites * an existing entry (mirrors the direct-mutation overwrite semantics). Arena-backed; * grows by copy. class_qn/field_name are arena-dup'd so the entry never borrows a * transient node-text buffer. */ static void py_overlay_register_field(PyLSPContext *ctx, const char *class_qn, const char *field_name, const CBMType *field_type) { for (int i = 0; i < ctx->field_overlay_count; i++) { if (strcmp(ctx->field_overlay[i].class_qn, class_qn) == 0 && strcmp(ctx->field_overlay[i].field_name, field_name) == 0) { ctx->field_overlay[i].field_type = field_type; return; } } if (ctx->field_overlay_count >= ctx->field_overlay_cap) { int new_cap = ctx->field_overlay_cap == 0 ? 16 : ctx->field_overlay_cap * 2; void *na = cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*ctx->field_overlay)); if (!na) return; /* OOM: drop this field (resolution degrades, never corrupts) */ if (ctx->field_overlay && ctx->field_overlay_count > 0) memcpy(na, ctx->field_overlay, (size_t)ctx->field_overlay_count * sizeof(*ctx->field_overlay)); ctx->field_overlay = na; ctx->field_overlay_cap = new_cap; } ctx->field_overlay[ctx->field_overlay_count].class_qn = cbm_arena_strdup(ctx->arena, class_qn); ctx->field_overlay[ctx->field_overlay_count].field_name = cbm_arena_strdup(ctx->arena, field_name); ctx->field_overlay[ctx->field_overlay_count].field_type = field_type; ctx->field_overlay_count++; } /* Look up a field on a registered type. Walks alias / embedded chain * with the same depth cap as method lookup. Returns the field's type * or NULL if no match. */ static const CBMType *py_lookup_field_depth(PyLSPContext *ctx, const char *type_qn, const char *field_name, int depth) { if (!ctx || !ctx->registry || !type_qn || !field_name) return NULL; if (depth > CBM_LSP_MAX_LOOKUP_DEPTH) return NULL; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn); if (!rt) return NULL; if (rt->field_names && rt->field_types) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], field_name) == 0) { return rt->field_types[i]; } } } /* Per-file overlay: `self.x = ...` fields discovered during resolve against the * sealed shared registry live here (not on rt). Checked at the same point the * direct field scan would have found them in the mutable path — preserving the * derived-shadows-base precedence before recursing into alias/base classes. */ { const CBMType *ov = py_overlay_lookup_field(ctx, type_qn, field_name); if (ov) return ov; } if (rt->alias_of) { const CBMType *a = py_lookup_field_depth(ctx, rt->alias_of, field_name, depth + 1); if (a) return a; } if (rt->embedded_types) { for (int i = 0; rt->embedded_types[i]; i++) { const CBMType *e = py_lookup_field_depth(ctx, rt->embedded_types[i], field_name, depth + 1); if (e) return e; } } return NULL; } static const CBMType *py_lookup_field(PyLSPContext *ctx, const char *type_qn, const char *field_name) { return py_lookup_field_depth(ctx, type_qn, field_name, 0); } /* Append a (name, type) entry to a registered class's field arrays. * Idempotent — re-registering the same name overwrites the existing * entry. Mutates the array in place by allocating a new (slightly * larger) array on each call; not a hot path so the cost is acceptable. */ static void py_register_instance_field(PyLSPContext *ctx, const char *class_qn, const char *field_name, const CBMType *field_type) { if (!ctx || !ctx->registry || !class_qn || !field_name || !field_type) return; /* Shared Tier-2 registry is finalized + sealed (read_only) and read concurrently * by all resolve workers. Writing its type entries directly below would bypass the * add_* seal, race the other workers, and leave the shared entry pointing into * this file's resolve arena (freed when the file completes). Route the discovery * to the per-file overlay instead; py_lookup_field consults it alongside the * shared base, so same-file `self.x`/PEP-526 resolution is preserved with zero * shared mutation. Mutable per-file registries (read_only == false) keep the * byte-identical direct write below. */ if (ctx->registry->read_only) { py_overlay_register_field(ctx, class_qn, field_name, field_type); return; } // Find the type entry. cbm_registry_lookup_type returns a const pointer; // we need a mutable pointer into the registry's array. CBMRegisteredType *rt = NULL; for (int i = 0; i < ctx->registry->type_count; i++) { const char *qn = ctx->registry->types[i].qualified_name; if (qn && strcmp(qn, class_qn) == 0) { rt = &ctx->registry->types[i]; break; } } if (!rt) return; // Overwrite if name already registered. if (rt->field_names && rt->field_types) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], field_name) == 0) { ((const CBMType **)rt->field_types)[i] = field_type; return; } } } int existing = 0; if (rt->field_names) { while (rt->field_names[existing]) existing++; } int new_count = existing + 1; const char **new_names = (const char **)cbm_arena_alloc(ctx->arena, (size_t)(new_count + 1) * sizeof(const char *)); const CBMType **new_types = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(new_count + 1) * sizeof(const CBMType *)); if (!new_names || !new_types) return; for (int i = 0; i < existing; i++) { new_names[i] = rt->field_names[i]; new_types[i] = rt->field_types[i]; } new_names[existing] = cbm_arena_strdup(ctx->arena, field_name); new_types[existing] = field_type; new_names[new_count] = NULL; new_types[new_count] = NULL; rt->field_names = new_names; rt->field_types = new_types; } /* ── expression typing ────────────────────────────────────────── */ /* Map a tree-sitter Python literal node to a BUILTIN type. Returns NULL if * the node isn't a simple literal. */ static const CBMType *py_literal_type(PyLSPContext *ctx, TSNode node) { const char *k = ts_node_type(node); if (strcmp(k, "integer") == 0) return cbm_type_builtin(ctx->arena, "int"); if (strcmp(k, "float") == 0) return cbm_type_builtin(ctx->arena, "float"); if (strcmp(k, "string") == 0) return cbm_type_builtin(ctx->arena, "str"); if (strcmp(k, "concatenated_string") == 0) return cbm_type_builtin(ctx->arena, "str"); if (strcmp(k, "true") == 0 || strcmp(k, "false") == 0) return cbm_type_builtin(ctx->arena, "bool"); if (strcmp(k, "none") == 0) return cbm_type_builtin(ctx->arena, "None"); // tuple / list / set / dict are handled separately by py_eval_expr_type // so it can build structured TUPLE / TEMPLATE types from element // types. Only fall through to BUILTIN here for empty literals. if (strcmp(k, "list_comprehension") == 0) return cbm_type_builtin(ctx->arena, "list"); if (strcmp(k, "dictionary_comprehension") == 0) return cbm_type_builtin(ctx->arena, "dict"); if (strcmp(k, "set_comprehension") == 0) return cbm_type_builtin(ctx->arena, "set"); if (strcmp(k, "generator_expression") == 0) return cbm_type_builtin(ctx->arena, "generator"); return NULL; } /* Substitute "Self" / "typing.Self" return types with the receiver type. * Walks the type recursively so `Optional[Self]` becomes `Optional[R]`. */ static const CBMType *py_substitute_self(PyLSPContext *ctx, const CBMType *t, const char *receiver_qn) { if (!t || !receiver_qn) return t; if (t->kind == CBM_TYPE_NAMED && t->data.named.qualified_name) { const char *qn = t->data.named.qualified_name; if (strcmp(qn, "Self") == 0 || strcmp(qn, "typing.Self") == 0 || strcmp(qn, "typing_extensions.Self") == 0) { return cbm_type_named(ctx->arena, receiver_qn); } } if (t->kind == CBM_TYPE_UNION) { int n = t->data.union_type.count; const CBMType **members = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (!members) return t; for (int i = 0; i < n; i++) { members[i] = py_substitute_self(ctx, t->data.union_type.members[i], receiver_qn); } return cbm_type_union(ctx->arena, members, n); } return t; } /* If `func_qn` is a registered function, return its return type. When * called on a receiver, pass receiver_qn to substitute Self return * types; pass NULL otherwise. */ static const CBMType *py_func_return_type_recv(PyLSPContext *ctx, const char *func_qn, const char *receiver_qn) { if (!ctx || !func_qn) return cbm_type_unknown(); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, func_qn); if (!f || !f->signature) return cbm_type_unknown(); if (f->signature->kind != CBM_TYPE_FUNC) return cbm_type_unknown(); const CBMType **rets = f->signature->data.func.return_types; if (!rets || !rets[0]) return cbm_type_unknown(); const CBMType *base; if (!rets[1]) { base = rets[0]; } else { int count = 0; while (rets[count]) count++; base = cbm_type_tuple(ctx->arena, rets, count); } if (receiver_qn) { return py_substitute_self(ctx, base, receiver_qn); } return base; } /* Convenience wrapper: no receiver substitution. */ static const CBMType *py_func_return_type(PyLSPContext *ctx, const char *func_qn) { return py_func_return_type_recv(ctx, func_qn, NULL); } /* Element type of an iterable. For TEMPLATE("list"|"set"|"Iterable"|..., * [T]), return T. For TEMPLATE("dict", [K, V]) — `for x in d` iterates * keys, return K. For TEMPLATE("tuple", [A, B, ...]) return UNION(A, B, * ...) since `for x in (a, b)` yields heterogeneous types. dict_items / * ItemsView return tuple[K, V] so unpacking works. NAMED("list") with * no template args -> UNKNOWN. */ /* Bind a `for X in ...` target. Supports identifier (single binding) and * pattern_list / tuple_pattern (destructure tuple element type). */ static void py_bind_for_target(PyLSPContext *ctx, TSNode left, const CBMType *elem_type) { if (ts_node_is_null(left) || !ctx) return; const char *lk = ts_node_type(left); if (strcmp(lk, "identifier") == 0) { char *name = py_node_text(ctx, left); if (name) py_scope_bind(ctx, name, elem_type); return; } if (strcmp(lk, "pattern_list") == 0 || strcmp(lk, "tuple_pattern") == 0 || strcmp(lk, "list_pattern") == 0) { // Destructure tuple element type element-by-element. const CBMType *const *elems = NULL; int count = 0; if (elem_type) { if (elem_type->kind == CBM_TYPE_TUPLE) { elems = elem_type->data.tuple.elems; count = elem_type->data.tuple.count; } else if (elem_type->kind == CBM_TYPE_TEMPLATE && elem_type->data.template_type.template_name && strcmp(elem_type->data.template_type.template_name, "tuple") == 0) { elems = elem_type->data.template_type.template_args; count = elem_type->data.template_type.arg_count; } } uint32_t lc = ts_node_named_child_count(left); for (uint32_t i = 0; i < lc; i++) { TSNode tgt = ts_node_named_child(left, i); if (ts_node_is_null(tgt)) continue; const char *tk = ts_node_type(tgt); if (strcmp(tk, "identifier") == 0) { char *nm = py_node_text(ctx, tgt); if (!nm) continue; const CBMType *bind_type; if (elems && (int)i < count && elems[i]) bind_type = elems[i]; else bind_type = cbm_type_unknown(); py_scope_bind(ctx, nm, bind_type); } } } } static const CBMType *py_iterable_element_type(PyLSPContext *ctx, const CBMType *iter_type) { if (!iter_type) return cbm_type_unknown(); if (iter_type->kind == CBM_TYPE_TEMPLATE) { const char *name = iter_type->data.template_type.template_name; const CBMType **args = iter_type->data.template_type.template_args; int n = iter_type->data.template_type.arg_count; if (!name || n <= 0 || !args) return cbm_type_unknown(); if (strcmp(name, "list") == 0 || strcmp(name, "set") == 0 || strcmp(name, "frozenset") == 0 || strcmp(name, "Iterable") == 0 || strcmp(name, "Iterator") == 0 || strcmp(name, "Sequence") == 0 || strcmp(name, "MutableSequence") == 0 || strcmp(name, "AsyncIterable") == 0 || strcmp(name, "AsyncIterator") == 0 || strcmp(name, "Generator") == 0 || strcmp(name, "AsyncGenerator") == 0 || strcmp(name, "Reversible") == 0 || strcmp(name, "Collection") == 0 || strcmp(name, "Container") == 0 || strcmp(name, "deque") == 0 || strcmp(name, "KeysView") == 0 || strcmp(name, "ValuesView") == 0 || strcmp(name, "dict_keys") == 0 || strcmp(name, "dict_values") == 0) { return args[0]; } if (strcmp(name, "ItemsView") == 0 || strcmp(name, "dict_items") == 0) { // Iterating ItemsView[K, V] yields tuple[K, V]. if (n >= 2) { const CBMType *elems[3] = {args[0], args[1], NULL}; return cbm_type_tuple(ctx->arena, elems, 2); } return cbm_type_unknown(); } if (strcmp(name, "dict") == 0 || strcmp(name, "Mapping") == 0 || strcmp(name, "MutableMapping") == 0 || strcmp(name, "defaultdict") == 0 || strcmp(name, "OrderedDict") == 0) { // `for k in d` iterates keys. return args[0]; } if (strcmp(name, "tuple") == 0) { if (n == 1) return args[0]; return cbm_type_union(ctx->arena, args, n); } // Other generics: best-effort take first arg. return args[0]; } return cbm_type_unknown(); } /* The real recursive-descent evaluator. Never call directly — go through * the memoizing, depth- and budget-guarded py_eval_expr_type wrapper below * (every recursive call inside this body already does). */ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return cbm_type_unknown(); const CBMType *lit = py_literal_type(ctx, node); if (lit) return lit; const char *k = ts_node_type(node); // Tuple / list / set / dict literals: build structured types from // element types so unpacking and indexing work without losing // precision. py_literal_type returns BUILTIN — we override here. if (strcmp(k, "tuple") == 0) { uint32_t cn = ts_node_named_child_count(node); if (cn == 0) return cbm_type_builtin(ctx->arena, "tuple"); const CBMType **elems = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(cn + 1) * sizeof(const CBMType *)); if (!elems) return cbm_type_builtin(ctx->arena, "tuple"); for (uint32_t i = 0; i < cn; i++) { elems[i] = py_eval_expr_type(ctx, ts_node_named_child(node, i)); } elems[cn] = NULL; return cbm_type_tuple(ctx->arena, elems, (int)cn); } if (strcmp(k, "list") == 0) { uint32_t cn = ts_node_named_child_count(node); if (cn == 0) return cbm_type_builtin(ctx->arena, "list"); const CBMType *first = py_eval_expr_type(ctx, ts_node_named_child(node, 0)); if (!first || cbm_type_is_unknown(first)) return cbm_type_builtin(ctx->arena, "list"); return cbm_type_template(ctx->arena, "list", &first, 1); } if (strcmp(k, "set") == 0) { uint32_t cn = ts_node_named_child_count(node); if (cn == 0) return cbm_type_builtin(ctx->arena, "set"); const CBMType *first = py_eval_expr_type(ctx, ts_node_named_child(node, 0)); if (!first || cbm_type_is_unknown(first)) return cbm_type_builtin(ctx->arena, "set"); return cbm_type_template(ctx->arena, "set", &first, 1); } if (strcmp(k, "dictionary") == 0) { uint32_t cn = ts_node_named_child_count(node); if (cn == 0) return cbm_type_builtin(ctx->arena, "dict"); for (uint32_t i = 0; i < cn; i++) { TSNode pair = ts_node_named_child(node, i); if (strcmp(ts_node_type(pair), "pair") != 0) continue; TSNode key = ts_node_child_by_field_name(pair, "key", 3); TSNode value = ts_node_child_by_field_name(pair, "value", 5); if (ts_node_is_null(key) || ts_node_is_null(value)) continue; const CBMType *kt = py_eval_expr_type(ctx, key); const CBMType *vt = py_eval_expr_type(ctx, value); if (!kt || !vt) break; const CBMType *args[3] = {kt, vt, NULL}; return cbm_type_template(ctx->arena, "dict", args, 2); } return cbm_type_builtin(ctx->arena, "dict"); } if (strcmp(k, "identifier") == 0) { char *name = py_node_text(ctx, node); if (!name) return cbm_type_unknown(); const CBMType *t = cbm_scope_lookup(ctx->current_scope, name); if (!cbm_type_is_unknown(t)) return t; // Builtin globals: True / False / None at top level. if (strcmp(name, "True") == 0 || strcmp(name, "False") == 0) return cbm_type_builtin(ctx->arena, "bool"); if (strcmp(name, "None") == 0) return cbm_type_builtin(ctx->arena, "None"); // Module/package-local function. const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name); if (f) return py_func_return_type(ctx, f->qualified_name); // Builtins fallback (range / len / list / str / int / etc.). For // builtin classes, this returns the class as NAMED so subsequent // attribute access works. f = cbm_registry_lookup_symbol(ctx->registry, "builtins", name); if (f) return py_func_return_type(ctx, f->qualified_name); const CBMRegisteredType *rt = cbm_registry_lookup_type( ctx->registry, cbm_arena_sprintf(ctx->arena, "builtins.%s", name)); if (rt) return cbm_type_builtin(ctx->arena, name); return cbm_type_unknown(); } if (strcmp(k, "attribute") == 0) { TSNode obj = ts_node_child_by_field_name(node, "object", 6); TSNode attr = ts_node_child_by_field_name(node, "attribute", 9); if (ts_node_is_null(obj) || ts_node_is_null(attr)) return cbm_type_unknown(); const CBMType *obj_type = py_eval_expr_type(ctx, obj); if (obj_type) obj_type = cbm_type_resolve_alias(obj_type); char *attr_name = py_node_text(ctx, attr); if (!attr_name || !obj_type) return cbm_type_unknown(); if (obj_type->kind == CBM_TYPE_MODULE) { // module.attr — return type of the registered symbol if known. const char *mod = obj_type->data.module.module_qn; const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mod, attr_name); if (f) return py_func_return_type(ctx, f->qualified_name); // Could also be a class — return NAMED("mod.attr") const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mod, attr_name); const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, qn); if (rt) return cbm_type_named(ctx->arena, qn); // Submodule: if any registered function/type has qn starting // with ".." then mod.attr is itself a module. // Linear scan over registry funcs is O(R) per access; we // skip it for the common case where mod.attr is already // matched as a function/type above. With ~900 stdlib funcs // and many module-attr accesses per file, this can dominate // — keeping the loop tight and bailing early on first match. const char *prefix = cbm_arena_sprintf(ctx->arena, "%s.", qn); size_t prefix_len = strlen(prefix); bool is_submodule = false; for (int i = 0; i < ctx->registry->func_count; i++) { const char *fqn = ctx->registry->funcs[i].qualified_name; if (fqn && strncmp(fqn, prefix, prefix_len) == 0) { is_submodule = true; break; } } if (is_submodule) return cbm_type_module(ctx->arena, qn); return cbm_type_unknown(); } if (obj_type->kind == CBM_TYPE_NAMED) { const CBMRegisteredFunc *f = py_lookup_attribute(ctx, obj_type->data.named.qualified_name, attr_name); if (f) return py_func_return_type_recv(ctx, f->qualified_name, obj_type->data.named.qualified_name); // Field fallback: instance attributes from __init__'s self.x = expr. const CBMType *field = py_lookup_field(ctx, obj_type->data.named.qualified_name, attr_name); if (field) return field; } if (obj_type->kind == CBM_TYPE_BUILTIN && obj_type->data.builtin.name) { // Builtins map to typeshed stdlib via "builtins.". const char *recv_qn = cbm_arena_sprintf(ctx->arena, "builtins.%s", obj_type->data.builtin.name); const CBMRegisteredFunc *f = py_lookup_attribute(ctx, recv_qn, attr_name); if (f) return py_func_return_type_recv(ctx, f->qualified_name, recv_qn); } if (obj_type->kind == CBM_TYPE_TEMPLATE && obj_type->data.template_type.template_name) { // dict[K, V].get / list[T].append etc — receiver is the // unparameterized container in typeshed: builtins.dict / // builtins.list / collections.abc.Mapping etc. const char *tname = obj_type->data.template_type.template_name; // Try builtins. first, then bare tname. const char *recv_qn = cbm_arena_sprintf(ctx->arena, "builtins.%s", tname); const CBMRegisteredFunc *f = py_lookup_attribute(ctx, recv_qn, attr_name); if (!f) { f = py_lookup_attribute(ctx, tname, attr_name); } if (f) return py_func_return_type_recv(ctx, f->qualified_name, recv_qn); } if (obj_type->kind == CBM_TYPE_UNION) { int matches = 0; const CBMRegisteredFunc *hit = NULL; const char *hit_recv = NULL; for (int i = 0; i < obj_type->data.union_type.count; i++) { const CBMType *m = obj_type->data.union_type.members[i]; if (!m || m->kind != CBM_TYPE_NAMED) continue; const CBMRegisteredFunc *f = py_lookup_attribute(ctx, m->data.named.qualified_name, attr_name); if (f) { matches++; hit = f; hit_recv = m->data.named.qualified_name; } } if (matches == 1 && hit) { return py_func_return_type_recv(ctx, hit->qualified_name, hit_recv); } // Field fallback for UNION: same single-match heuristic. int field_matches = 0; const CBMType *field_hit = NULL; for (int i = 0; i < obj_type->data.union_type.count; i++) { const CBMType *m = obj_type->data.union_type.members[i]; if (!m || m->kind != CBM_TYPE_NAMED) continue; const CBMType *fld = py_lookup_field(ctx, m->data.named.qualified_name, attr_name); if (fld) { field_matches++; field_hit = fld; } } if (field_matches == 1 && field_hit) return field_hit; } return cbm_type_unknown(); } if (strcmp(k, "call") == 0) { TSNode fn = ts_node_child_by_field_name(node, "function", 8); if (ts_node_is_null(fn)) return cbm_type_unknown(); const char *fk = ts_node_type(fn); // Container method special-cases: dict.items() / .keys() / .values(), // list.copy(), set.copy() etc. Without a constraint solver we can't // substitute K, V into the registered method's return type, so we // hand-roll the most-impactful container methods. if (strcmp(fk, "attribute") == 0) { TSNode obj = ts_node_child_by_field_name(fn, "object", 6); TSNode attr = ts_node_child_by_field_name(fn, "attribute", 9); if (!ts_node_is_null(obj) && !ts_node_is_null(attr)) { const CBMType *obj_type = py_eval_expr_type(ctx, obj); char *mname = py_node_text(ctx, attr); if (mname && obj_type && obj_type->kind == CBM_TYPE_TEMPLATE) { const char *tname = obj_type->data.template_type.template_name; const CBMType **args = obj_type->data.template_type.template_args; int n = obj_type->data.template_type.arg_count; bool is_dict_like = tname && (strcmp(tname, "dict") == 0 || strcmp(tname, "Mapping") == 0 || strcmp(tname, "MutableMapping") == 0 || strcmp(tname, "defaultdict") == 0 || strcmp(tname, "OrderedDict") == 0); bool is_list_like = tname && (strcmp(tname, "list") == 0 || strcmp(tname, "set") == 0 || strcmp(tname, "frozenset") == 0 || strcmp(tname, "deque") == 0); if (is_dict_like && args && n >= 2) { if (strcmp(mname, "items") == 0) { const CBMType *pair[3] = {args[0], args[1], NULL}; return cbm_type_template(ctx->arena, "ItemsView", pair, 2); } if (strcmp(mname, "keys") == 0) { const CBMType *k1[2] = {args[0], NULL}; return cbm_type_template(ctx->arena, "KeysView", k1, 1); } if (strcmp(mname, "values") == 0) { const CBMType *v1[2] = {args[1], NULL}; return cbm_type_template(ctx->arena, "ValuesView", v1, 1); } if (strcmp(mname, "get") == 0) { // dict.get(k) -> Optional[V] return cbm_type_optional(ctx->arena, args[1]); } if (strcmp(mname, "pop") == 0) { return args[1]; } if (strcmp(mname, "copy") == 0) { return obj_type; // dict[K, V] } } if (is_list_like && args && n >= 1) { if (strcmp(mname, "copy") == 0 || strcmp(mname, "__iter__") == 0) { return obj_type; } if (strcmp(mname, "pop") == 0) { return args[0]; } } } } } // typing.cast(T, x) -> NAMED(T). typing.assert_type(x, T) -> type-of(x). // Detect by the call's function expression: matches either bare `cast` / // `assert_type` (when imported from typing) or `typing.cast` style. bool is_cast = false; bool is_assert_type = false; if (strcmp(fk, "identifier") == 0) { char *nm = py_node_text(ctx, fn); if (nm) { is_cast = strcmp(nm, "cast") == 0; is_assert_type = strcmp(nm, "assert_type") == 0; } } else if (strcmp(fk, "attribute") == 0) { TSNode aobj = ts_node_child_by_field_name(fn, "object", 6); TSNode aattr = ts_node_child_by_field_name(fn, "attribute", 9); if (!ts_node_is_null(aobj) && !ts_node_is_null(aattr) && strcmp(ts_node_type(aobj), "identifier") == 0) { char *mod = py_node_text(ctx, aobj); char *nm = py_node_text(ctx, aattr); if (mod && nm && strcmp(mod, "typing") == 0) { is_cast = strcmp(nm, "cast") == 0; is_assert_type = strcmp(nm, "assert_type") == 0; } } } if (is_cast || is_assert_type) { TSNode args = ts_node_child_by_field_name(node, "arguments", 9); if (!ts_node_is_null(args) && ts_node_named_child_count(args) >= 2) { if (is_cast) { TSNode type_arg = ts_node_named_child(args, 0); char *type_text = py_node_text(ctx, type_arg); if (type_text) return py_resolve_annotation(ctx, type_text); } else { // assert_type(x, T) returns x's type unchanged (it's a no-op). TSNode val_arg = ts_node_named_child(args, 0); return py_eval_expr_type(ctx, val_arg); } } } if (strcmp(fk, "identifier") == 0) { char *fname = py_node_text(ctx, fn); if (!fname) return cbm_type_unknown(); // next(iter) — returns the iterable's element type. if (strcmp(fname, "next") == 0) { TSNode args = ts_node_child_by_field_name(node, "arguments", 9); if (!ts_node_is_null(args) && ts_node_named_child_count(args) >= 1) { const CBMType *iter_type = py_eval_expr_type(ctx, ts_node_named_child(args, 0)); const CBMType *elem = py_iterable_element_type(ctx, iter_type); if (elem && !cbm_type_is_unknown(elem)) return elem; } } // iter(x) -> Iterator[element_of(x)] (best-effort: same as x). if (strcmp(fname, "iter") == 0) { TSNode args = ts_node_child_by_field_name(node, "arguments", 9); if (!ts_node_is_null(args) && ts_node_named_child_count(args) >= 1) { return py_eval_expr_type(ctx, ts_node_named_child(args, 0)); } } // Constructor call: ClassName() returns NAMED(ClassName). const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname); if (!cbm_type_is_unknown(in_scope)) { if (in_scope->kind == CBM_TYPE_NAMED) { const CBMRegisteredType *rt = cbm_registry_lookup_type( ctx->registry, in_scope->data.named.qualified_name); if (rt) return in_scope; // NAMED(ClassName) = instance type } // CALLABLE in scope -> calling it returns its return type. if (in_scope->kind == CBM_TYPE_CALLABLE) { return in_scope->data.callable.return_type ? in_scope->data.callable.return_type : cbm_type_unknown(); } // TEMPLATE("Callable", [..., R]) — last template arg is the // return type; calling such a value yields R. if (in_scope->kind == CBM_TYPE_TEMPLATE && in_scope->data.template_type.template_name && strcmp(in_scope->data.template_type.template_name, "Callable") == 0 && in_scope->data.template_type.arg_count >= 1) { int n = in_scope->data.template_type.arg_count; return in_scope->data.template_type.template_args[n - 1]; } } // Module-local function call. const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, fname); if (f) return py_func_return_type(ctx, f->qualified_name); // Builtins fallback: range / len / list / str / int / etc. f = cbm_registry_lookup_symbol(ctx->registry, "builtins", fname); if (f) return py_func_return_type(ctx, f->qualified_name); // Builtin class constructor: list() / dict() / set() / str() / ... const CBMRegisteredType *rt = cbm_registry_lookup_type( ctx->registry, cbm_arena_sprintf(ctx->arena, "builtins.%s", fname)); if (rt) return cbm_type_builtin(ctx->arena, fname); } // factory()() / (lambda x: x)(...) / etc — evaluate the function // expression's type, and if it's a callable, return its return. if (strcmp(fk, "call") == 0 || strcmp(fk, "parenthesized_expression") == 0 || strcmp(fk, "lambda") == 0) { const CBMType *fn_type = py_eval_expr_type(ctx, fn); if (fn_type && fn_type->kind == CBM_TYPE_CALLABLE) { return fn_type->data.callable.return_type ? fn_type->data.callable.return_type : cbm_type_unknown(); } if (fn_type && fn_type->kind == CBM_TYPE_TEMPLATE && fn_type->data.template_type.template_name && strcmp(fn_type->data.template_type.template_name, "Callable") == 0 && fn_type->data.template_type.arg_count >= 1) { int n = fn_type->data.template_type.arg_count; return fn_type->data.template_type.template_args[n - 1]; } } if (strcmp(fk, "attribute") == 0) { TSNode obj = ts_node_child_by_field_name(fn, "object", 6); TSNode attr = ts_node_child_by_field_name(fn, "attribute", 9); if (ts_node_is_null(obj) || ts_node_is_null(attr)) return cbm_type_unknown(); const CBMType *obj_type = py_eval_expr_type(ctx, obj); char *attr_name = py_node_text(ctx, attr); if (!attr_name || !obj_type) return cbm_type_unknown(); if (obj_type->kind == CBM_TYPE_MODULE) { const char *mod = obj_type->data.module.module_qn; const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mod, attr_name); if (f) return py_func_return_type(ctx, f->qualified_name); } if (obj_type->kind == CBM_TYPE_NAMED) { const CBMRegisteredFunc *f = py_lookup_attribute(ctx, obj_type->data.named.qualified_name, attr_name); if (f) return py_func_return_type_recv(ctx, f->qualified_name, obj_type->data.named.qualified_name); } } return cbm_type_unknown(); } if (strcmp(k, "binary_operator") == 0) { TSNode left = ts_node_child_by_field_name(node, "left", 4); TSNode op = ts_node_child_by_field_name(node, "operator", 8); const CBMType *lt = py_eval_expr_type(ctx, left); // Try the operator's dunder method on left's type. Falls back to // left-operand type when registry has no info (most common: int+int, // str+str, list+list — typing-by-left is correct in those cases). if (lt && lt->kind == CBM_TYPE_NAMED && !ts_node_is_null(op)) { char *op_text = py_node_text(ctx, op); const char *dunder = NULL; if (op_text) { if (strcmp(op_text, "+") == 0) dunder = "__add__"; else if (strcmp(op_text, "-") == 0) dunder = "__sub__"; else if (strcmp(op_text, "*") == 0) dunder = "__mul__"; else if (strcmp(op_text, "/") == 0) dunder = "__truediv__"; else if (strcmp(op_text, "//") == 0) dunder = "__floordiv__"; else if (strcmp(op_text, "%") == 0) dunder = "__mod__"; else if (strcmp(op_text, "**") == 0) dunder = "__pow__"; else if (strcmp(op_text, "<<") == 0) dunder = "__lshift__"; else if (strcmp(op_text, ">>") == 0) dunder = "__rshift__"; else if (strcmp(op_text, "&") == 0) dunder = "__and__"; else if (strcmp(op_text, "|") == 0) dunder = "__or__"; else if (strcmp(op_text, "^") == 0) dunder = "__xor__"; else if (strcmp(op_text, "@") == 0) dunder = "__matmul__"; } if (dunder) { const CBMRegisteredFunc *f = py_lookup_attribute(ctx, lt->data.named.qualified_name, dunder); if (f) { return py_func_return_type_recv(ctx, f->qualified_name, lt->data.named.qualified_name); } } } return lt ? lt : cbm_type_unknown(); } if (strcmp(k, "comparison_operator") == 0 || strcmp(k, "boolean_operator") == 0 || strcmp(k, "not_operator") == 0) { return cbm_type_builtin(ctx->arena, "bool"); } if (strcmp(k, "conditional_expression") == 0) { // a if cond else b — return union of a, b types uint32_t nc = ts_node_named_child_count(node); if (nc >= 2) { const CBMType *a = py_eval_expr_type(ctx, ts_node_named_child(node, 0)); const CBMType *b = py_eval_expr_type(ctx, ts_node_named_child(node, 2)); const CBMType *members[2] = {a, b}; return cbm_type_union(ctx->arena, members, 2); } } if (strcmp(k, "parenthesized_expression") == 0) { if (ts_node_named_child_count(node) > 0) { return py_eval_expr_type(ctx, ts_node_named_child(node, 0)); } } // `await expr` — for resolution purposes we treat as identity. async // functions register their declared return type as the registered // return; callers use it directly without modeling Coroutine[A, A, T]. if (strcmp(k, "await") == 0 || strcmp(k, "await_expression") == 0) { if (ts_node_named_child_count(node) > 0) { return py_eval_expr_type(ctx, ts_node_named_child(node, 0)); } } // Subscript: container[index]. For TEMPLATE("dict", [K, V])[K] -> V; // TEMPLATE("list"|"set"|...)[int] -> elem; TEMPLATE("tuple", [A, B, // ...])[int] -> A (union when index isn't a literal int). NAMED that // resolves to a TypedDict-tagged registered type with a literal-string // key returns the field's type. if (strcmp(k, "subscript") == 0) { TSNode value = ts_node_child_by_field_name(node, "value", 5); if (ts_node_is_null(value)) return cbm_type_unknown(); const CBMType *container = py_eval_expr_type(ctx, value); // TypedDict literal-key subscript: d["foo"] where d: TD and TD has // class-body annotation `foo: Foo`. Field is registered via // py_register_instance_field. if (container && container->kind == CBM_TYPE_NAMED) { TSNode sub = ts_node_child_by_field_name(node, "subscript", 9); if (!ts_node_is_null(sub)) { const char *sk = ts_node_type(sub); if (strcmp(sk, "string") == 0) { char *lit = py_node_text(ctx, sub); if (lit) { // Strip surrounding quotes. size_t llen = strlen(lit); if (llen >= 2 && (lit[0] == '"' || lit[0] == '\'') && lit[llen - 1] == lit[0]) { lit[llen - 1] = '\0'; lit++; } const CBMType *field = py_lookup_field(ctx, container->data.named.qualified_name, lit); if (field) return field; } } } return cbm_type_unknown(); } if (!container || container->kind != CBM_TYPE_TEMPLATE) { return cbm_type_unknown(); } const char *tname = container->data.template_type.template_name; const CBMType **args = container->data.template_type.template_args; int n = container->data.template_type.arg_count; if (!tname || !args || n <= 0) return cbm_type_unknown(); // Slice subscript returns the same container type: // list[T][1:3] -> list[T]; str[1:3] -> str (handled via BUILTIN // path elsewhere). TSNode sub = ts_node_child_by_field_name(node, "subscript", 9); if (!ts_node_is_null(sub) && strcmp(ts_node_type(sub), "slice") == 0) { return container; } if (strcmp(tname, "dict") == 0 || strcmp(tname, "Mapping") == 0 || strcmp(tname, "MutableMapping") == 0 || strcmp(tname, "defaultdict") == 0 || strcmp(tname, "OrderedDict") == 0 || strcmp(tname, "ChainMap") == 0) { // Value type is the second template arg. if (n >= 2 && args[1]) return args[1]; return cbm_type_unknown(); } if (strcmp(tname, "list") == 0 || strcmp(tname, "set") == 0 || strcmp(tname, "frozenset") == 0 || strcmp(tname, "Sequence") == 0 || strcmp(tname, "MutableSequence") == 0 || strcmp(tname, "deque") == 0 || strcmp(tname, "Iterable") == 0 || strcmp(tname, "Iterator") == 0) { return args[0]; } if (strcmp(tname, "tuple") == 0) { if (!ts_node_is_null(sub) && strcmp(ts_node_type(sub), "integer") == 0) { char *idx_text = py_node_text(ctx, sub); if (idx_text) { int idx = atoi(idx_text); if (idx >= 0 && idx < n) return args[idx]; } } if (n == 1) return args[0]; return cbm_type_union(ctx->arena, args, n); } // Other generics: best-effort first arg. return args[0]; } return cbm_type_unknown(); } /* ── py_eval_expr_type memoization + guards (issues #710, #720) ────────── * * py_eval_expr_type_uncached re-evaluates a call node's attribute receiver * twice — once in the container special-case and again in the general * attribute path — so an N-link chained call (`Builder().add(x)...add(z)`) * cost O(2^N) evaluations: the #710 indexing hang (~65-link real chains). * Memoizing per node makes each distinct node evaluate at most once per * scope generation, and py_emit_call_for's per-call-node receiver * re-evaluation becomes an O(1) cache hit. * * Key: the tree-sitter node identity pointer (TSNode.id — unique per node * within one parse tree). NOT the node's start byte: in a chained call * every leftmost descendant (the whole chain, each receiver, down to the * root identifier) shares the same start byte, so byte-keyed entries alias * distinct nodes and silently return the wrong type (missing/incorrect * CALLS edges). * * Entries carry the scope generation (see py_scope_bind); stale * generations never match, which keeps caching invisible to resolution * behavior. The table is open-addressing/linear-probe, arena-allocated on * the per-file arena (freed with it; ctx is memset per file so each file * starts cold). */ enum { PY_TYPE_CACHE_INITIAL_CAP = 256 }; /* power of two (index masking) */ enum { PY_TYPE_CACHE_MAX_CAP = 1 << 26 }; /* hard stop for adversarial files */ static uint32_t py_type_cache_hash(const void *id) { uintptr_t v = (uintptr_t)id >> 3; /* node structs are >=8-byte aligned */ return (uint32_t)((v ^ (v >> 29)) * 2654435761u); } /* Returns the cached full-fidelity result for this node, or NULL when the * node has no entry from the current scope generation. Live entries never * store NULL (the wrapper normalizes to cbm_type_unknown() first), so NULL * unambiguously means "miss". */ static const CBMType *py_type_cache_lookup(const PyLSPContext *ctx, const void *id) { if (!ctx->type_cache || ctx->type_cache_cap == 0 || !id) return NULL; uint32_t mask = (uint32_t)ctx->type_cache_cap - 1; uint32_t idx = py_type_cache_hash(id) & mask; for (int probed = 0; probed < ctx->type_cache_cap; probed++) { const CBMPyTypeCacheEntry *e = &ctx->type_cache[idx]; if (!e->node_id) return NULL; /* empty slot: this id was never inserted */ if (e->node_id == id) return e->gen == ctx->type_cache_gen ? e->result : NULL; idx = (idx + 1) & mask; } return NULL; /* probe bound: unreachable below the 75% load ceiling */ } /* Doubles the table (arena allocation: the old table is abandoned and * freed with the per-file arena). Entries from stale generations are * dropped during the rehash — they can never match again. */ static bool py_type_cache_grow(PyLSPContext *ctx) { if (ctx->type_cache_cap >= PY_TYPE_CACHE_MAX_CAP) return false; int new_cap = ctx->type_cache_cap ? ctx->type_cache_cap * 2 : PY_TYPE_CACHE_INITIAL_CAP; CBMPyTypeCacheEntry *new_entries = (CBMPyTypeCacheEntry *)cbm_arena_alloc( ctx->arena, (size_t)new_cap * sizeof(CBMPyTypeCacheEntry)); if (!new_entries) return false; memset(new_entries, 0, (size_t)new_cap * sizeof(CBMPyTypeCacheEntry)); CBMPyTypeCacheEntry *old_entries = ctx->type_cache; int old_cap = ctx->type_cache_cap; ctx->type_cache = new_entries; ctx->type_cache_cap = new_cap; ctx->type_cache_count = 0; uint32_t mask = (uint32_t)new_cap - 1; for (int i = 0; i < old_cap; i++) { if (!old_entries[i].node_id || old_entries[i].gen != ctx->type_cache_gen) continue; uint32_t idx = py_type_cache_hash(old_entries[i].node_id) & mask; while (new_entries[idx].node_id) idx = (idx + 1) & mask; new_entries[idx] = old_entries[i]; ctx->type_cache_count++; } return true; } static void py_type_cache_insert(PyLSPContext *ctx, const void *id, const CBMType *result) { if (!id || !result) return; /* Keep load strictly below 75% so probe chains stay short. If growth * fails (OOM / max cap), REJECT the insert rather than filling up: a * full table would turn every probe loop into a full-table scan, and * an unbounded insert loop on a full table would spin forever. */ if (!ctx->type_cache || (ctx->type_cache_count + 1) * 4 > ctx->type_cache_cap * 3) { if (!py_type_cache_grow(ctx) && (!ctx->type_cache || (ctx->type_cache_count + 1) * 4 > ctx->type_cache_cap * 3)) return; } uint32_t mask = (uint32_t)ctx->type_cache_cap - 1; uint32_t idx = py_type_cache_hash(id) & mask; for (int probed = 0; probed < ctx->type_cache_cap; probed++) { CBMPyTypeCacheEntry *e = &ctx->type_cache[idx]; if (!e->node_id) { e->node_id = id; e->gen = ctx->type_cache_gen; e->result = result; ctx->type_cache_count++; return; } if (e->node_id == id) { e->gen = ctx->type_cache_gen; /* refresh a stale-generation entry in place */ e->result = result; return; } idx = (idx + 1) & mask; } /* Probe bound exhausted — only reachable with a corrupt count; drop the * insert instead of spinning. */ } /* Memoizing, depth- and budget-guarded wrapper — the function every call * site in this file goes through. */ static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return cbm_type_unknown(); /* Depth cap (issue #720): the evaluator recurses once per expression * nesting level, so a pathologically deep expression (tens of * thousands of parens) overflowed the native stack. Same limit as * C_EVAL_DEPTH_LIMIT. Past the cap: unknown, and NEVER cached. */ if (ctx->eval_depth >= PY_LSP_MAX_EVAL_DEPTH) { ctx->eval_truncations++; return cbm_type_unknown(); } const CBMType *cached = py_type_cache_lookup(ctx, node.id); if (cached) return cached; /* Per-file work budget (mirrors C_EVAL_MAX_STEPS_PER_FILE): expression * type evaluation is best-effort, so pathological files degrade to * unknown instead of stalling repository indexing. Only real * evaluations consume budget — cache hits above are O(1). */ if (ctx->eval_steps++ > PY_EVAL_MAX_STEPS_PER_FILE) { ctx->eval_truncations++; if (ctx->debug && ctx->eval_steps == PY_EVAL_MAX_STEPS_PER_FILE + 2) { fprintf(stderr, " [pylsp] expression eval step budget exhausted; returning unknown\n"); } return cbm_type_unknown(); } uint32_t trunc_before = ctx->eval_truncations; ctx->eval_depth++; const CBMType *result = py_eval_expr_type_uncached(ctx, node); ctx->eval_depth--; if (!result) result = cbm_type_unknown(); /* Only cache full-fidelity results: if any descendant evaluation was * cut off by the depth cap or the step budget, this result may be a * truncated `unknown`. Caching it would poison later evaluations that * reach the same node from a shallower frame (or with fresh budget) — * exactly the reuse the guards are supposed to preserve. */ if (ctx->eval_truncations == trunc_before) py_type_cache_insert(ctx, node.id, result); return result; } /* ── statement processing: bind from assignments, for-loops, with-as ──── */ static void py_process_statement(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return; const char *k = ts_node_type(node); if (strcmp(k, "assignment") == 0) { TSNode left = ts_node_child_by_field_name(node, "left", 4); TSNode right = ts_node_child_by_field_name(node, "right", 5); TSNode ann = ts_node_child_by_field_name(node, "type", 4); const CBMType *rhs_type = ts_node_is_null(right) ? cbm_type_unknown() : py_eval_expr_type(ctx, right); // Annotated assignment: x: T = expr — annotation wins. bool has_annotation = !ts_node_is_null(ann); if (has_annotation) { char *ann_text = py_node_text(ctx, ann); if (ann_text && ann_text[0]) { rhs_type = py_resolve_annotation(ctx, ann_text); } } if (ts_node_is_null(left)) return; const char *lk = ts_node_type(left); // Tuple/list pattern unpacking: a, b = f() / [a, b] = f() / // (a, b) = f(). Each LHS element binds to the corresponding // element of the RHS tuple. *rest binds the remaining elements // as a list (best-effort: list of element type). if (strcmp(lk, "pattern_list") == 0 || strcmp(lk, "tuple_pattern") == 0 || strcmp(lk, "list_pattern") == 0 || strcmp(lk, "expression_list") == 0) { uint32_t lc = ts_node_named_child_count(left); // Collect RHS element types. const CBMType *const *rhs_elems = NULL; int rhs_count = 0; if (rhs_type) { if (rhs_type->kind == CBM_TYPE_TUPLE) { rhs_elems = rhs_type->data.tuple.elems; rhs_count = rhs_type->data.tuple.count; } else if (rhs_type->kind == CBM_TYPE_TEMPLATE && rhs_type->data.template_type.template_name) { const char *tn = rhs_type->data.template_type.template_name; if (strcmp(tn, "tuple") == 0) { rhs_elems = rhs_type->data.template_type.template_args; rhs_count = rhs_type->data.template_type.arg_count; } } } // Element type for star-rest binding (best-effort element of // the iterable; for a pure tuple use UNKNOWN since the rest // is heterogeneous). const CBMType *elem_type = py_iterable_element_type(ctx, rhs_type); for (uint32_t i = 0; i < lc; i++) { TSNode tgt = ts_node_named_child(left, i); if (ts_node_is_null(tgt)) continue; const char *tk = ts_node_type(tgt); bool is_rest = false; TSNode bind_target = tgt; if (strcmp(tk, "list_splat_pattern") == 0 || strcmp(tk, "list_splat") == 0) { is_rest = true; if (ts_node_named_child_count(tgt) > 0) { bind_target = ts_node_named_child(tgt, 0); } } if (strcmp(ts_node_type(bind_target), "identifier") != 0) continue; char *nm = py_node_text(ctx, bind_target); if (!nm) continue; const CBMType *bind_type; if (is_rest) { bind_type = elem_type ? cbm_type_template(ctx->arena, "list", &elem_type, 1) : cbm_type_unknown(); } else if (rhs_elems && (int)i < rhs_count && rhs_elems[i]) { bind_type = rhs_elems[i]; } else { bind_type = elem_type ? elem_type : cbm_type_unknown(); } py_scope_bind(ctx, nm, bind_type); } return; } if (strcmp(lk, "identifier") == 0) { char *name = py_node_text(ctx, left); if (name) { py_scope_bind(ctx, name, rhs_type); // Lambda registry: `fn = lambda x: ...`. if (!ts_node_is_null(right) && strcmp(ts_node_type(right), "lambda") == 0) { py_register_lambda(ctx, name, right); } // Dict literal dispatch table: `funcs = {"a": foo, "b": bar}` // with all values being known function QNs. if (!ts_node_is_null(right) && strcmp(ts_node_type(right), "dictionary") == 0) { py_register_dict_literal(ctx, name, right); } } } else if (strcmp(lk, "attribute") == 0) { // self.x = expr — record as instance field on the enclosing // class. Effective inside any method that resolves obj.x where // obj is an instance of the class. TSNode obj = ts_node_child_by_field_name(left, "object", 6); TSNode attr = ts_node_child_by_field_name(left, "attribute", 9); if (!ts_node_is_null(obj) && !ts_node_is_null(attr) && strcmp(ts_node_type(obj), "identifier") == 0 && ctx->enclosing_class_qn) { char *obj_name = py_node_text(ctx, obj); char *attr_name = py_node_text(ctx, attr); if (obj_name && attr_name && (strcmp(obj_name, "self") == 0 || strcmp(obj_name, "cls") == 0)) { py_register_instance_field(ctx, ctx->enclosing_class_qn, attr_name, rhs_type); } } } return; } if (strcmp(k, "for_statement") == 0) { TSNode left = ts_node_child_by_field_name(node, "left", 4); TSNode right = ts_node_child_by_field_name(node, "right", 5); const CBMType *elem_type = cbm_type_unknown(); if (!ts_node_is_null(right)) { const CBMType *iter_type = py_eval_expr_type(ctx, right); elem_type = py_iterable_element_type(ctx, iter_type); // For async for: try unwrapping AsyncIterator[T] / AsyncIterable[T] // / AsyncGenerator[T, S] — already handled in py_iterable_element_type. // If the iterable came from an explicit __aiter__ / __anext__ // call, look up __anext__'s return type. if (cbm_type_is_unknown(elem_type) && iter_type && iter_type->kind == CBM_TYPE_NAMED) { const CBMRegisteredFunc *anext = py_lookup_attribute(ctx, iter_type->data.named.qualified_name, "__anext__"); if (anext) { const CBMType *ret = py_func_return_type_recv( ctx, anext->qualified_name, iter_type->data.named.qualified_name); if (ret && !cbm_type_is_unknown(ret)) elem_type = ret; } } } py_bind_for_target(ctx, left, elem_type); return; } // async_with_statement (with_statement preceded by `async`) — same // shape as with_statement; tree-sitter Python uses with_statement // for both, with an `async` token. async with binds via __aenter__. if (strcmp(k, "with_statement") == 0) { // with X as y: bind y to X.__enter__() return type. Tree-sitter // Python wraps `X as y` in an `as_pattern` whose first named child // is X (value) and second is the target identifier `y` (alias). // The with_item may either contain the as_pattern as its child, or // directly expose value/alias as field children — we handle both. uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(node, i); if (strcmp(ts_node_type(child), "with_clause") != 0) continue; uint32_t cn = ts_node_named_child_count(child); for (uint32_t j = 0; j < cn; j++) { TSNode item = ts_node_named_child(child, j); if (strcmp(ts_node_type(item), "with_item") != 0) continue; // Try field lookup first; fall back to as_pattern walk. TSNode value = ts_node_child_by_field_name(item, "value", 5); TSNode alias = ts_node_child_by_field_name(item, "alias", 5); if (ts_node_is_null(value) || ts_node_is_null(alias)) { // Look for an as_pattern child. uint32_t ic = ts_node_named_child_count(item); for (uint32_t k2 = 0; k2 < ic; k2++) { TSNode c = ts_node_named_child(item, k2); if (strcmp(ts_node_type(c), "as_pattern") == 0) { uint32_t ac = ts_node_named_child_count(c); if (ac >= 1) value = ts_node_named_child(c, 0); if (ac >= 2) alias = ts_node_named_child(c, 1); // Field-based lookup as another option. if (ts_node_is_null(alias)) { alias = ts_node_child_by_field_name(c, "alias", 5); } break; } } } if (ts_node_is_null(alias) || strcmp(ts_node_type(alias), "identifier") != 0) { // alias may itself be an as_pattern_target wrapping an // identifier; walk through it. if (!ts_node_is_null(alias) && ts_node_named_child_count(alias) > 0) { TSNode inner = ts_node_named_child(alias, 0); if (strcmp(ts_node_type(inner), "identifier") == 0) { alias = inner; } } } if (ts_node_is_null(alias) || strcmp(ts_node_type(alias), "identifier") != 0) continue; char *name = py_node_text(ctx, alias); if (!name) continue; const CBMType *cm_type = ts_node_is_null(value) ? cbm_type_unknown() : py_eval_expr_type(ctx, value); const CBMType *bind_type = cm_type; if (cm_type && cm_type->kind == CBM_TYPE_NAMED) { // Try __aenter__ first (async with), fall back to // __enter__ (sync with). __aenter__ is a coroutine // returning T; we treat the return as T directly. const CBMRegisteredFunc *enter = py_lookup_attribute(ctx, cm_type->data.named.qualified_name, "__aenter__"); if (!enter) { enter = py_lookup_attribute(ctx, cm_type->data.named.qualified_name, "__enter__"); } if (enter) { const CBMType *ret = py_func_return_type_recv( ctx, enter->qualified_name, cm_type->data.named.qualified_name); if (ret && !cbm_type_is_unknown(ret)) { bind_type = ret; } } } py_scope_bind(ctx, name, bind_type); } } return; } if (strcmp(k, "try_statement") == 0) { // except E as e: bind e to NAMED(E). Tree-sitter Python wraps // `E as e` either as an as_pattern child of the except_clause, // or as flat children. Handle both shapes. uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode clause = ts_node_named_child(node, i); if (strcmp(ts_node_type(clause), "except_clause") != 0) continue; TSNode exc_type = {0}; TSNode exc_name = {0}; uint32_t ccn = ts_node_named_child_count(clause); for (uint32_t j = 0; j < ccn; j++) { TSNode c = ts_node_named_child(clause, j); const char *ck = ts_node_type(c); if (strcmp(ck, "block") == 0) continue; if (strcmp(ck, "as_pattern") == 0) { uint32_t ac = ts_node_named_child_count(c); if (ac >= 1) exc_type = ts_node_named_child(c, 0); if (ac >= 2) exc_name = ts_node_named_child(c, 1); // alias may be wrapped in as_pattern_target if (!ts_node_is_null(exc_name)) { const char *nk = ts_node_type(exc_name); if (strcmp(nk, "as_pattern_target") == 0 && ts_node_named_child_count(exc_name) > 0) { exc_name = ts_node_named_child(exc_name, 0); } } continue; } if (ts_node_is_null(exc_type)) exc_type = c; else if (ts_node_is_null(exc_name) && strcmp(ck, "identifier") == 0) { exc_name = c; } } if (!ts_node_is_null(exc_name) && !ts_node_is_null(exc_type) && strcmp(ts_node_type(exc_name), "identifier") == 0) { char *nm = py_node_text(ctx, exc_name); char *tn = py_node_text(ctx, exc_type); if (nm && tn) { py_scope_bind(ctx, nm, py_resolve_annotation(ctx, tn)); } } } return; } } /* ── Recursive walker: process statements + emit resolved_calls ── */ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) { TSNode fn = ts_node_child_by_field_name(call_node, "function", 8); if (ts_node_is_null(fn)) return; const char *fk = ts_node_type(fn); if (strcmp(fk, "identifier") == 0) { char *fname = py_node_text(ctx, fn); if (!fname) return; // Lambda invocation: `fn(args)` where fn was assigned a lambda. // Walk the lambda body for any nested call sites with the params // bound to the call's arg types. Resolved calls get caller_qn // set to a synthetic child of the enclosing function. TSNode lambda_node = py_lookup_lambda(ctx, fname); if (!ts_node_is_null(lambda_node)) { TSNode params = ts_node_child_by_field_name(lambda_node, "parameters", 10); TSNode body = ts_node_child_by_field_name(lambda_node, "body", 4); TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); // Bind each lambda param to the call-site arg's type. if (!ts_node_is_null(params) && !ts_node_is_null(args)) { uint32_t pn = ts_node_named_child_count(params); uint32_t an = ts_node_named_child_count(args); for (uint32_t pi = 0; pi < pn && pi < an; pi++) { TSNode p = ts_node_named_child(params, pi); TSNode a = ts_node_named_child(args, pi); const char *pname = NULL; if (strcmp(ts_node_type(p), "identifier") == 0) { pname = py_node_text(ctx, p); } else if (strcmp(ts_node_type(p), "default_parameter") == 0 || strcmp(ts_node_type(p), "typed_parameter") == 0) { TSNode pi_n = ts_node_child_by_field_name(p, "name", 4); if (!ts_node_is_null(pi_n) && strcmp(ts_node_type(pi_n), "identifier") == 0) { pname = py_node_text(ctx, pi_n); } } if (pname) { const CBMType *arg_type = py_eval_expr_type(ctx, a); py_scope_bind(ctx, pname, arg_type); } } } // Set enclosing func to a synthetic so emissions // attribute correctly (find_resolved by "" works). const char *prev_func = ctx->enclosing_func_qn; ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.", prev_func ? prev_func : ctx->module_qn); if (!ts_node_is_null(body)) { py_resolve_calls_in(ctx, body); } ctx->enclosing_func_qn = prev_func; py_scope_restore(ctx, saved); return; } // Constructor call (ClassName()) const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname); if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) { const char *qn = in_scope->data.named.qualified_name; py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f); return; } // Module-local function const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, fname); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f); return; } // Builtins (range / len / list / dict / str / int / print / ...). f = cbm_registry_lookup_symbol(ctx->registry, "builtins", fname); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_builtin", 0.92f); return; } const CBMRegisteredType *rt = cbm_registry_lookup_type( ctx->registry, cbm_arena_sprintf(ctx->arena, "builtins.%s", fname)); if (rt) { py_emit_resolved_call(ctx, rt->qualified_name, "lsp_builtin_constructor", 0.88f); return; } return; } // Subscript-as-call: `funcs["a"]()` where funcs is a registered // dict-literal dispatch table. if (strcmp(fk, "subscript") == 0) { TSNode container = ts_node_child_by_field_name(fn, "value", 5); TSNode key = ts_node_child_by_field_name(fn, "subscript", 9); if (!ts_node_is_null(container) && !ts_node_is_null(key) && strcmp(ts_node_type(container), "identifier") == 0 && strcmp(ts_node_type(key), "string") == 0) { char *var_name = py_node_text(ctx, container); char *k_text = py_string_literal_value(ctx, key); if (var_name && k_text) { const char *tgt = py_lookup_dict_dispatch(ctx, var_name, k_text); if (tgt) { /* The textual callee of `funcs["a"](v)` is the subscript base * identifier ("funcs"), not the resolved target ("foo"), so * stash it in `reason` for the join (see lsp_resolve.h). */ py_emit_resolved_call_reason(ctx, tgt, "lsp_dict_dispatch", 0.86f, var_name); return; } } } } if (strcmp(fk, "attribute") == 0) { TSNode obj = ts_node_child_by_field_name(fn, "object", 6); TSNode attr = ts_node_child_by_field_name(fn, "attribute", 9); if (ts_node_is_null(obj) || ts_node_is_null(attr)) return; char *attr_name = py_node_text(ctx, attr); if (!attr_name) return; // super().method() — Python 3 super() returns a proxy bound to the // enclosing class's MRO. Resolve attr against the first base class // of the enclosing class (single-inheritance practical case). if (strcmp(ts_node_type(obj), "call") == 0) { TSNode super_fn = ts_node_child_by_field_name(obj, "function", 8); if (!ts_node_is_null(super_fn) && strcmp(ts_node_type(super_fn), "identifier") == 0) { char *super_name = py_node_text(ctx, super_fn); if (super_name && strcmp(super_name, "super") == 0 && ctx->enclosing_class_qn) { const CBMRegisteredType *enclosing = cbm_registry_lookup_type(ctx->registry, ctx->enclosing_class_qn); if (enclosing && enclosing->embedded_types) { for (int i = 0; enclosing->embedded_types[i]; i++) { // super().__init__() is a constructor delegation: // lsp_super_init is the MORE SPECIFIC, more accurate // strategy than the generic lsp_super. Resolve __init__ // first and emit lsp_super_init — when the base both // registers __init__ (py_lookup_attribute hits) and the // generic super() proxy resolution applies, the generic // lsp_super used to also be emitted at 0.88, outranking // lsp_super_init (0.85) in the highest-confidence join so // the specific strategy never landed on the edge. Handle // __init__ BEFORE the generic lsp_super and rank it at // least as high (0.90) so the constructor-delegation // strategy wins. The plain super().method() form below is // unchanged — it still emits lsp_super. if (strcmp(attr_name, "__init__") == 0) { const CBMRegisteredFunc *fi = py_lookup_attribute( ctx, enclosing->embedded_types[i], attr_name); const char *init_qn = fi ? fi->qualified_name : cbm_arena_sprintf(ctx->arena, "%s.__init__", enclosing->embedded_types[i]); py_emit_resolved_call(ctx, init_qn, "lsp_super_init", 0.90f); return; } const CBMRegisteredFunc *f = py_lookup_attribute(ctx, enclosing->embedded_types[i], attr_name); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_super", 0.88f); return; } } } } } } const CBMType *obj_type = py_eval_expr_type(ctx, obj); if (!obj_type) return; if (obj_type->kind == CBM_TYPE_MODULE) { const char *mod = obj_type->data.module.module_qn; const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mod, attr_name); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_module_attr", 0.92f); return; } // An `import sibling` of an IN-PROJECT module records the module's QN // in its short, source-written form ("helpers"), but the sibling's // defs are registered project-qualified (".helpers.do_work"). // So the lookup above misses for in-project modules even though the // target IS resolvable, and the call used to drop to // lsp_module_attr_unresolved @0.55 (below the join's 0.6 floor) — no // edge. Retry against the project-qualified module: derive the // project root from the current file's module_qn (strip its last // segment) and look up ".". A genuinely-external module // (requests, os) has no such project def, so it correctly stays // lsp_module_attr_unresolved. if (mod && ctx->module_qn) { const char *last_dot = strrchr(ctx->module_qn, '.'); if (last_dot && last_dot > ctx->module_qn) { size_t root_len = (size_t)(last_dot - ctx->module_qn); // Skip if mod is already rooted under the project to avoid // "..mod". if (!(strncmp(mod, ctx->module_qn, root_len) == 0 && mod[root_len] == '.')) { char *qual_mod = (char *)cbm_arena_alloc(ctx->arena, root_len + 1 + strlen(mod) + 1); if (qual_mod) { memcpy(qual_mod, ctx->module_qn, root_len); qual_mod[root_len] = '.'; strcpy(qual_mod + root_len + 1, mod); const CBMRegisteredFunc *qf = cbm_registry_lookup_symbol(ctx->registry, qual_mod, attr_name); if (qf) { py_emit_resolved_call(ctx, qf->qualified_name, "lsp_module_attr", 0.92f); return; } } } } } // Best-effort: emit "module.attr" QN — Phase 9 cross-file may fix up. const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mod, attr_name); py_emit_resolved_call(ctx, qn, "lsp_module_attr_unresolved", 0.55f); return; } if (obj_type->kind == CBM_TYPE_NAMED) { const CBMRegisteredFunc *f = py_lookup_attribute(ctx, obj_type->data.named.qualified_name, attr_name); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_method", 0.9f); return; } } if (obj_type->kind == CBM_TYPE_BUILTIN && obj_type->data.builtin.name) { // Builtins -> "builtins." in typeshed. const char *recv_qn = cbm_arena_sprintf(ctx->arena, "builtins.%s", obj_type->data.builtin.name); const CBMRegisteredFunc *f = py_lookup_attribute(ctx, recv_qn, attr_name); if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_builtin_method", 0.9f); return; } } if (obj_type->kind == CBM_TYPE_TEMPLATE && obj_type->data.template_type.template_name) { const char *tname = obj_type->data.template_type.template_name; const char *recv_qn = cbm_arena_sprintf(ctx->arena, "builtins.%s", tname); const CBMRegisteredFunc *f = py_lookup_attribute(ctx, recv_qn, attr_name); if (!f) { f = py_lookup_attribute(ctx, tname, attr_name); } if (f) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_generic_method", 0.88f); return; } } if (obj_type->kind == CBM_TYPE_UNION) { // Try each non-None member; if exactly one matches, emit. int matches = 0; const CBMRegisteredFunc *hit = NULL; for (int i = 0; i < obj_type->data.union_type.count; i++) { const CBMType *m = obj_type->data.union_type.members[i]; if (!m) continue; if (m->kind == CBM_TYPE_BUILTIN && m->data.builtin.name && strcmp(m->data.builtin.name, "None") == 0) continue; if (m->kind != CBM_TYPE_NAMED) continue; const CBMRegisteredFunc *f = py_lookup_attribute(ctx, m->data.named.qualified_name, attr_name); if (f) { matches++; hit = f; } } if (matches == 1 && hit) { py_emit_resolved_call(ctx, hit->qualified_name, "lsp_method_union", 0.85f); return; } } return; } } /* Detect `isinstance(x, T)` / `isinstance(x, (T1, T2))` and return * (var_name, narrowed_type). Returns true on match. */ static bool py_match_isinstance(PyLSPContext *ctx, TSNode call_node, char **out_name, const CBMType **out_type) { if (ts_node_is_null(call_node)) return false; if (strcmp(ts_node_type(call_node), "call") != 0) return false; TSNode fn = ts_node_child_by_field_name(call_node, "function", 8); if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "identifier") != 0) return false; char *fname = py_node_text(ctx, fn); if (!fname || strcmp(fname, "isinstance") != 0) return false; TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); if (ts_node_is_null(args) || ts_node_named_child_count(args) < 2) return false; TSNode var_node = ts_node_named_child(args, 0); TSNode type_node = ts_node_named_child(args, 1); if (ts_node_is_null(var_node) || strcmp(ts_node_type(var_node), "identifier") != 0) return false; char *name = py_node_text(ctx, var_node); if (!name) return false; char *tname = py_node_text(ctx, type_node); if (!tname) return false; *out_name = name; *out_type = py_resolve_annotation(ctx, tname); return true; } /* Walk through parenthesized expressions and walrus expressions to find * the underlying identifier of an `is None` operand. Returns the * identifier's text or NULL. */ static char *py_underlying_ident(PyLSPContext *ctx, TSNode node) { if (ts_node_is_null(node)) return NULL; const char *k = ts_node_type(node); if (strcmp(k, "identifier") == 0) { return py_node_text(ctx, node); } if (strcmp(k, "parenthesized_expression") == 0) { if (ts_node_named_child_count(node) > 0) { return py_underlying_ident(ctx, ts_node_named_child(node, 0)); } } if (strcmp(k, "named_expression") == 0 || strcmp(k, "assignment_expression") == 0) { TSNode left = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(left)) { left = ts_node_child_by_field_name(node, "left", 4); } if (!ts_node_is_null(left) && strcmp(ts_node_type(left), "identifier") == 0) { return py_node_text(ctx, left); } } return NULL; } /* Detect `x is None` / `x is not None` / `None is x` / etc. Walks * through parens and walrus to find the underlying identifier. * Returns 1 for "x is not None" (positive narrow), -1 for "x is None" * (negative), 0 otherwise. */ static int py_match_is_none(PyLSPContext *ctx, TSNode test_node, char **out_name) { if (ts_node_is_null(test_node)) return 0; const char *k = ts_node_type(test_node); if (strcmp(k, "comparison_operator") == 0) { uint32_t cn = ts_node_child_count(test_node); TSNode left = {0}, right = {0}; bool is_not = false; bool is_is = false; bool first_named = true; for (uint32_t i = 0; i < cn; i++) { TSNode c = ts_node_child(test_node, i); if (ts_node_is_null(c)) continue; if (ts_node_is_named(c)) { if (first_named) { left = c; first_named = false; } else { right = c; } continue; } // Anonymous operator token. Tree-sitter Python emits "is" or // "is not" as a single token via the sym__is_not literal. char *tok = py_node_text(ctx, c); if (!tok) continue; if (strcmp(tok, "is") == 0) is_is = true; else if (strcmp(tok, "is not") == 0) { is_is = true; is_not = true; } } if (!is_is || ts_node_is_null(left) || ts_node_is_null(right)) return 0; char *l_text = py_node_text(ctx, left); char *r_text = py_node_text(ctx, right); if (!l_text || !r_text) return 0; char *var_name = NULL; if (strcmp(r_text, "None") == 0) { var_name = py_underlying_ident(ctx, left); } else if (strcmp(l_text, "None") == 0) { var_name = py_underlying_ident(ctx, right); } if (!var_name) return 0; *out_name = var_name; return is_not ? 1 : -1; } return 0; } /* Compute the non-None component of a UNION type. If t is Optional[X] * (= Union[X, None]), return X; otherwise return t unchanged. */ static const CBMType *py_strip_none(PyLSPContext *ctx, const CBMType *t) { if (!t || t->kind != CBM_TYPE_UNION) return t; int n = t->data.union_type.count; int retained = 0; const CBMType **kept = (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (!kept) return t; for (int i = 0; i < n; i++) { const CBMType *m = t->data.union_type.members[i]; if (m && m->kind == CBM_TYPE_BUILTIN && m->data.builtin.name && strcmp(m->data.builtin.name, "None") == 0) continue; kept[retained++] = m; } if (retained == 0) return cbm_type_unknown(); if (retained == 1) return kept[0]; return cbm_type_union(ctx->arena, kept, retained); } /* Walk a node's subtree looking for walrus expressions and bind their * targets in the enclosing function scope (PEP 572 semantics). */ static void py_bind_walrus_in(PyLSPContext *ctx, TSNode node) { if (ts_node_is_null(node)) return; const char *k = ts_node_type(node); if (strcmp(k, "named_expression") == 0 || strcmp(k, "assignment_expression") == 0) { TSNode left = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(left)) { left = ts_node_child_by_field_name(node, "left", 4); } TSNode right = ts_node_child_by_field_name(node, "value", 5); if (ts_node_is_null(right)) { right = ts_node_child_by_field_name(node, "right", 5); } if (!ts_node_is_null(left) && !ts_node_is_null(right) && strcmp(ts_node_type(left), "identifier") == 0) { char *name = py_node_text(ctx, left); if (name) { const CBMType *t = py_eval_expr_type(ctx, right); py_scope_bind(ctx, name, t); } } } uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { py_bind_walrus_in(ctx, ts_node_named_child(node, i)); } } /* Detect whether a block ends with `return`, `raise`, `break`, or * `continue` — meaning execution doesn't fall through past the if-block. * Used for early-return narrowing of the negated condition. */ static bool py_block_terminates(TSNode block) { if (ts_node_is_null(block)) return false; uint32_t bnc = ts_node_named_child_count(block); if (bnc == 0) return false; TSNode last = ts_node_named_child(block, bnc - 1); const char *k = ts_node_type(last); return strcmp(k, "return_statement") == 0 || strcmp(k, "raise_statement") == 0 || strcmp(k, "break_statement") == 0 || strcmp(k, "continue_statement") == 0; } /* Walk if_statement: push scope for consequence body when narrowing * applies, then recurse. If the consequence terminates (return/raise), * apply the negated narrow to the *enclosing* scope so subsequent * statements see the narrowed type. */ static void py_walk_if_statement(PyLSPContext *ctx, TSNode if_node) { TSNode cond = ts_node_child_by_field_name(if_node, "condition", 9); TSNode body = ts_node_child_by_field_name(if_node, "consequence", 11); TSNode alt = ts_node_child_by_field_name(if_node, "alternative", 11); // Walrus bindings in the condition leak into the enclosing scope. py_bind_walrus_in(ctx, cond); // Always evaluate the condition (may bind via walrus, currently a no-op). py_resolve_calls_in(ctx, cond); // Recognize negated isinstance / is-None patterns for early-return // narrowing. bool neg_isinstance = false; char *neg_name = NULL; const CBMType *neg_type = NULL; int neg_none_kind = 0; char *neg_none_var = NULL; if (!ts_node_is_null(cond) && strcmp(ts_node_type(cond), "not_operator") == 0 && ts_node_named_child_count(cond) > 0) { TSNode inner = ts_node_named_child(cond, 0); if (strcmp(ts_node_type(inner), "call") == 0) { char *nm = NULL; const CBMType *ty = NULL; if (py_match_isinstance(ctx, inner, &nm, &ty)) { neg_isinstance = true; neg_name = nm; neg_type = ty; } } // `if not (x is None):` -> equivalent to `if x is not None` // (rare, the next branch handles `is not None` directly). } // `if x is None: return` — after the if block, x is non-None. { char *nv = NULL; int nk = py_match_is_none(ctx, cond, &nv); if (nk == -1) { // x is None neg_none_kind = -1; neg_none_var = nv; } } // Consequence: maybe narrow. if (!ts_node_is_null(body)) { CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); // Try isinstance narrowing. if (!ts_node_is_null(cond) && strcmp(ts_node_type(cond), "call") == 0) { char *name = NULL; const CBMType *narrowed = NULL; if (py_match_isinstance(ctx, cond, &name, &narrowed) && name && narrowed) { py_scope_bind(ctx, name, narrowed); } } // Try `x is not None` narrowing. char *none_var = NULL; int none_kind = py_match_is_none(ctx, cond, &none_var); if (none_kind == 1 && none_var) { const CBMType *current = cbm_scope_lookup(ctx->current_scope, none_var); if (current && !cbm_type_is_unknown(current)) { const CBMType *narrowed = py_strip_none(ctx, current); py_scope_bind(ctx, none_var, narrowed); } } py_resolve_calls_in(ctx, body); py_scope_restore(ctx, saved); } // Alternative branch: include else / elif. Recurse without narrowing — // the else branch's narrowing is the negation, which we don't model in v1. if (!ts_node_is_null(alt)) { py_resolve_calls_in(ctx, alt); } // Early-return narrowing: if the consequence block terminates and the // condition negates a type guard, apply the *positive* narrow to the // enclosing scope so subsequent statements see the narrowed type. if (!ts_node_is_null(body) && py_block_terminates(body)) { if (neg_isinstance && neg_name && neg_type) { py_scope_bind(ctx, neg_name, neg_type); } if (neg_none_kind == -1 && neg_none_var) { // `if x is None: return` -> narrow x to non-None afterwards. const CBMType *current = cbm_scope_lookup(ctx->current_scope, neg_none_var); if (current && !cbm_type_is_unknown(current)) { const CBMType *narrowed = py_strip_none(ctx, current); py_scope_bind(ctx, neg_none_var, narrowed); } } } } /* Map a Python infix operator token to its dunder method name. */ static const char *py_binop_dunder(const char *op_text) { if (!op_text) return NULL; if (strcmp(op_text, "+") == 0) return "__add__"; if (strcmp(op_text, "-") == 0) return "__sub__"; if (strcmp(op_text, "*") == 0) return "__mul__"; if (strcmp(op_text, "/") == 0) return "__truediv__"; if (strcmp(op_text, "//") == 0) return "__floordiv__"; if (strcmp(op_text, "%") == 0) return "__mod__"; if (strcmp(op_text, "**") == 0) return "__pow__"; if (strcmp(op_text, "<<") == 0) return "__lshift__"; if (strcmp(op_text, ">>") == 0) return "__rshift__"; if (strcmp(op_text, "&") == 0) return "__and__"; if (strcmp(op_text, "|") == 0) return "__or__"; if (strcmp(op_text, "^") == 0) return "__xor__"; if (strcmp(op_text, "@") == 0) return "__matmul__"; return NULL; } /* If `recv` is a user-defined NAMED type that defines `dunder`, emit a CALLS * edge to that dunder method (operator-overload / subscript desugaring). This * models `a + b` → T.__add__ and `s[k]` → T.__getitem__ as calls. * * Requires a typed receiver. An UNTYPED receiver (e.g. the unannotated * parameter in `def run(s): return s[0]`) is intentionally left unresolved: * guessing the sole class that declares the dunder would mis-resolve ordinary * built-in subscripts/operators (`some_list[0]`, `a + b` on ints) onto an * unrelated user class, so we only resolve when the receiver type is known. */ static void py_emit_dunder_call(PyLSPContext *ctx, const CBMType *recv, const char *dunder) { if (!recv || recv->kind != CBM_TYPE_NAMED || !dunder) return; const CBMRegisteredFunc *f = py_lookup_attribute(ctx, recv->data.named.qualified_name, dunder); if (f && f->qualified_name) { py_emit_resolved_call(ctx, f->qualified_name, "lsp_operator_dunder", 0.85f); /* A subscript (`s[k]`) / binary_operator (`a + b`) is not a syntactic * `call` node, so the extractor produced no CBMCall for it and the * resolved_call above would never be matched into a CALLS edge. Inject * a synthetic CBMCall keyed on (enclosing_func_qn, short dunder name) * so resolve_file_calls can pair it with the resolved entry. The * resolved callee QN ends in the dunder, so its short name == dunder. * Mirrors rust_inject_syn_call. */ if (ctx->syn_calls && ctx->arena && ctx->enclosing_func_qn) { CBMCall call; memset(&call, 0, sizeof(call)); call.callee_name = cbm_arena_strdup(ctx->arena, dunder); call.enclosing_func_qn = ctx->enclosing_func_qn; cbm_calls_push(ctx->syn_calls, ctx->arena, call); } } } static void py_resolve_calls_in_inner(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return; const char *k = ts_node_type(node); // Statement-level binding effects. py_process_statement(ctx, node); // Emit call entry if applicable. if (strcmp(k, "call") == 0) { py_emit_call_for(ctx, node); } // Operator-overload desugaring: `a + b` calls type(a).__add__, // `s[k]` calls type(s).__getitem__. Only emit when the receiver is a // user-defined type that actually declares the dunder (built-ins resolve // to typeshed and would create noisy edges). if (strcmp(k, "binary_operator") == 0) { TSNode left = ts_node_child_by_field_name(node, "left", 4); TSNode op = ts_node_child_by_field_name(node, "operator", 8); if (!ts_node_is_null(left) && !ts_node_is_null(op)) { const char *dunder = py_binop_dunder(py_node_text(ctx, op)); if (dunder) { py_emit_dunder_call(ctx, py_eval_expr_type(ctx, left), dunder); } } } else if (strcmp(k, "subscript") == 0) { TSNode value = ts_node_child_by_field_name(node, "value", 5); if (!ts_node_is_null(value)) { py_emit_dunder_call(ctx, py_eval_expr_type(ctx, value), "__getitem__"); } } // if_statement gets special-case narrowing. if (strcmp(k, "if_statement") == 0) { py_walk_if_statement(ctx, node); return; } // match/case (PEP 634): subject narrows per case-pattern. if (strcmp(k, "match_statement") == 0) { TSNode subject = ts_node_child_by_field_name(node, "subject", 7); TSNode body = ts_node_child_by_field_name(node, "body", 4); // Subject identifier (best-effort — only narrow when the subject // is a single identifier). char *subject_name = NULL; if (!ts_node_is_null(subject) && strcmp(ts_node_type(subject), "identifier") == 0) { subject_name = py_node_text(ctx, subject); } py_resolve_calls_in(ctx, subject); if (!ts_node_is_null(body)) { uint32_t bcc = ts_node_named_child_count(body); for (uint32_t i = 0; i < bcc; i++) { TSNode case_clause = ts_node_named_child(body, i); if (strcmp(ts_node_type(case_clause), "case_clause") != 0) continue; CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); // Pattern is the first non-block named child; consequence // is the block. TSNode pattern = {0}, conseq = {0}; uint32_t cnc = ts_node_named_child_count(case_clause); for (uint32_t j = 0; j < cnc; j++) { TSNode c = ts_node_named_child(case_clause, j); const char *ck = ts_node_type(c); if (strcmp(ck, "block") == 0) { conseq = c; break; } if (ts_node_is_null(pattern)) pattern = c; } // The pattern field is a `case_pattern` wrapper around the // actual pattern type (class_pattern, sequence_pattern, etc). // Some grammar versions wrap further in `pattern`. Walk // through both wrappers. TSNode actual_pattern = pattern; for (int unwrap = 0; unwrap < 3; unwrap++) { if (ts_node_is_null(actual_pattern)) break; const char *apk = ts_node_type(actual_pattern); if ((strcmp(apk, "case_pattern") == 0 || strcmp(apk, "pattern") == 0 || strcmp(apk, "_simple_pattern") == 0) && ts_node_named_child_count(actual_pattern) > 0) { actual_pattern = ts_node_named_child(actual_pattern, 0); continue; } break; } // Class pattern: case Foo(a, b): -> bind subject to NAMED(Foo), // bind a / b to UNKNOWN (field-type extraction is a v1.1 task). if (subject_name && !ts_node_is_null(actual_pattern) && strcmp(ts_node_type(actual_pattern), "class_pattern") == 0) { if (ts_node_named_child_count(actual_pattern) > 0) { TSNode cls = ts_node_named_child(actual_pattern, 0); char *cls_text = py_node_text(ctx, cls); if (cls_text) { const CBMType *narrowed = py_resolve_annotation(ctx, cls_text); py_scope_bind(ctx, subject_name, narrowed); } } // Bind capture sub-patterns to UNKNOWN. for (uint32_t j = 1; j < ts_node_named_child_count(actual_pattern); j++) { TSNode sub = ts_node_named_child(actual_pattern, j); if (strcmp(ts_node_type(sub), "capture_pattern") == 0) { if (ts_node_named_child_count(sub) > 0) { TSNode id = ts_node_named_child(sub, 0); char *nm = py_node_text(ctx, id); if (nm) py_scope_bind(ctx, nm, cbm_type_unknown()); } } } } // Sequence pattern: `case [head, *tail]:` / `case (a, b):` // — for each capture pattern element, bind to the // subject's iterable element type. Star-rest binds to // list[elem]. Tree-sitter wraps each list_pattern child // in another case_pattern; a capture appears as a // dotted_name -> identifier; a splat as splat_pattern. if (subject_name && !ts_node_is_null(actual_pattern) && (strcmp(ts_node_type(actual_pattern), "list_pattern") == 0 || strcmp(ts_node_type(actual_pattern), "tuple_pattern") == 0)) { const CBMType *subj_t = cbm_scope_lookup(ctx->current_scope, subject_name); const CBMType *elem_t = py_iterable_element_type(ctx, subj_t); uint32_t pn = ts_node_named_child_count(actual_pattern); for (uint32_t j = 0; j < pn; j++) { TSNode elem = ts_node_named_child(actual_pattern, j); // Unwrap inner case_pattern. if (!ts_node_is_null(elem) && strcmp(ts_node_type(elem), "case_pattern") == 0 && ts_node_named_child_count(elem) > 0) { elem = ts_node_named_child(elem, 0); } if (ts_node_is_null(elem)) continue; const char *ek = ts_node_type(elem); // Capture pattern: dotted_name wrapping identifier. if (strcmp(ek, "dotted_name") == 0 && ts_node_named_child_count(elem) > 0) { TSNode id = ts_node_named_child(elem, 0); if (strcmp(ts_node_type(id), "identifier") == 0) { char *nm = py_node_text(ctx, id); if (nm && elem_t) { py_scope_bind(ctx, nm, elem_t); } } continue; } if (strcmp(ek, "identifier") == 0) { char *nm = py_node_text(ctx, elem); if (nm && elem_t) { py_scope_bind(ctx, nm, elem_t); } continue; } if (strcmp(ek, "splat_pattern") == 0 || strcmp(ek, "list_splat_pattern") == 0 || strcmp(ek, "star_pattern") == 0) { // splat_pattern's first named child is the // identifier (or another wrapper). TSNode id = {0}; uint32_t snc = ts_node_named_child_count(elem); for (uint32_t s = 0; s < snc; s++) { TSNode c = ts_node_named_child(elem, s); if (strcmp(ts_node_type(c), "identifier") == 0) { id = c; break; } if (strcmp(ts_node_type(c), "dotted_name") == 0 && ts_node_named_child_count(c) > 0) { id = ts_node_named_child(c, 0); break; } } if (!ts_node_is_null(id)) { char *nm = py_node_text(ctx, id); if (nm && elem_t) { const CBMType *lst = cbm_type_template(ctx->arena, "list", &elem_t, 1); py_scope_bind(ctx, nm, lst); } } } } } if (!ts_node_is_null(conseq)) { py_resolve_calls_in(ctx, conseq); } py_scope_restore(ctx, saved); } } return; } // Comprehensions push a scope and bind for_in_clause loop vars to the // iterable's element type, then walk inner expressions. if (strcmp(k, "list_comprehension") == 0 || strcmp(k, "dictionary_comprehension") == 0 || strcmp(k, "set_comprehension") == 0 || strcmp(k, "generator_expression") == 0) { CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); uint32_t cnc = ts_node_named_child_count(node); // First pass: bind for-clause vars (process in source order so // chained comprehensions like `for x in xs for y in x.ys` see // x bound by the time we evaluate x.ys). for (uint32_t i = 0; i < cnc; i++) { TSNode child = ts_node_named_child(node, i); const char *ck = ts_node_type(child); if (strcmp(ck, "for_in_clause") == 0) { TSNode left = ts_node_child_by_field_name(child, "left", 4); TSNode right = ts_node_child_by_field_name(child, "right", 5); const CBMType *elem_type = cbm_type_unknown(); if (!ts_node_is_null(right)) { const CBMType *iter_type = py_eval_expr_type(ctx, right); elem_type = py_iterable_element_type(ctx, iter_type); } py_bind_for_target(ctx, left, elem_type); } } // Second pass: recurse into all children (body + filter clauses // benefit from the bound vars). for (uint32_t i = 0; i < cnc; i++) { py_resolve_calls_in(ctx, ts_node_named_child(node, i)); } py_scope_restore(ctx, saved); return; } // Recurse: children. We don't push scope for control-flow blocks // here (Python scoping is function-level apart from comprehension / // lambda / class), but we do for nested function / class / lambda. if (strcmp(k, "function_definition") == 0 || strcmp(k, "class_definition") == 0 || strcmp(k, "lambda") == 0) { // These are processed by the top-level pass; skip recursion to // avoid double-walking their bodies. return; } uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { py_resolve_calls_in(ctx, ts_node_named_child(node, i)); } } /* ── function / class processing ───────────────────────────────── */ /* Parse a type-annotation TEXT into a CBMType without requiring a * PyLSPContext. Handles `Optional[T]`, `Union[A, B]`, `X | Y`, generic * containers (`list[T]` -> TEMPLATE), forward-reference quotes, and * builtin scalar names. Used by py_register_def, which runs before any * context is available. When module_qn is non-NULL, bare class names * are qualified as "." so registry lookups match. */ static const CBMType *py_parse_type_text(CBMArena *arena, const char *ann); static const CBMType *py_parse_type_text_qn(CBMArena *arena, const char *ann, const char *module_qn); /* Trim ASCII whitespace from both ends of an arena-allocated copy. */ static char *py_trim_ws(CBMArena *arena, const char *start, size_t len) { while (len > 0 && (start[0] == ' ' || start[0] == '\t')) { start++; len--; } while (len > 0 && (start[len - 1] == ' ' || start[len - 1] == '\t')) len--; char *out = (char *)cbm_arena_alloc(arena, len + 1); if (!out) return NULL; memcpy(out, start, len); out[len] = '\0'; return out; } /* Split a comma-separated argument string at depth 0 (ignoring commas * inside [], (), {}). Returns NULL-terminated arena array of trimmed * substring copies. Caller passes the inside of `[...]`. */ static const char **py_split_subscript_args(CBMArena *arena, const char *s, int *out_n) { if (!s) { *out_n = 0; return NULL; } int cap = 8; const char **out = (const char **)cbm_arena_alloc(arena, (size_t)(cap + 1) * sizeof(const char *)); if (!out) { *out_n = 0; return NULL; } int n = 0; int depth = 0; size_t len = strlen(s); size_t start = 0; for (size_t i = 0; i <= len; i++) { char c = (i < len) ? s[i] : ','; if (c == '[' || c == '(' || c == '{') depth++; else if (c == ']' || c == ')' || c == '}') depth--; else if (c == ',' && depth == 0) { char *part = py_trim_ws(arena, s + start, i - start); if (part && part[0]) { if (n >= cap) { int new_cap = cap * 2; const char **grown = (const char **)cbm_arena_alloc( arena, (size_t)(new_cap + 1) * sizeof(const char *)); if (grown) { for (int q = 0; q < n; q++) grown[q] = out[q]; out = grown; cap = new_cap; } } if (n < cap) out[n++] = part; } start = i + 1; } } out[n] = NULL; *out_n = n; return out; } static const CBMType *py_parse_type_text_qn(CBMArena *arena, const char *ann, const char *module_qn) { if (!ann || !ann[0]) return cbm_type_unknown(); size_t len = strlen(ann); if (len >= 2 && (ann[0] == '"' || ann[0] == '\'') && ann[len - 1] == ann[0]) { char *unquoted = (char *)cbm_arena_alloc(arena, len - 1); if (unquoted) { memcpy(unquoted, ann + 1, len - 2); unquoted[len - 2] = '\0'; return py_parse_type_text_qn(arena, unquoted, module_qn); } } const char *lb = strchr(ann, '['); if (lb && len > 0 && ann[len - 1] == ']') { size_t base_len = (size_t)(lb - ann); char *base = (char *)cbm_arena_alloc(arena, base_len + 1); if (base) { memcpy(base, ann, base_len); base[base_len] = '\0'; char *btrim = base; while (*btrim == ' ') btrim++; size_t blen = strlen(btrim); while (blen > 0 && btrim[blen - 1] == ' ') { btrim[blen - 1] = '\0'; blen--; } size_t inner_start = (size_t)(lb - ann) + 1; size_t inner_len = len - inner_start - 1; char *args_text = (char *)cbm_arena_alloc(arena, inner_len + 1); if (args_text) { memcpy(args_text, ann + inner_start, inner_len); args_text[inner_len] = '\0'; int arg_n = 0; const char **arg_strs = py_split_subscript_args(arena, args_text, &arg_n); const CBMType **arg_types = NULL; if (arg_n > 0 && arg_strs) { arg_types = (const CBMType **)cbm_arena_alloc( arena, (size_t)(arg_n + 1) * sizeof(const CBMType *)); if (arg_types) { for (int i = 0; i < arg_n; i++) { arg_types[i] = py_parse_type_text_qn(arena, arg_strs[i], module_qn); } arg_types[arg_n] = NULL; } } if (strcmp(btrim, "Optional") == 0 || strcmp(btrim, "typing.Optional") == 0) { if (arg_types && arg_n >= 1 && arg_types[0]) { return cbm_type_optional(arena, arg_types[0]); } } if (strcmp(btrim, "Union") == 0 || strcmp(btrim, "typing.Union") == 0) { if (arg_types && arg_n > 0) { return cbm_type_union(arena, arg_types, arg_n); } } // Type-wrapper annotations that don't change the underlying // type for resolution: ClassVar[T], Final[T], InitVar[T], // ReadOnly[T], Required[T], NotRequired[T], Annotated[T, ...] // (drop metadata), Mapped[T] (SQLAlchemy 2.0), and // typing_extensions/typing variants of all these. Returns // the wrapped type T. static const char *wrapper_names[] = {"ClassVar", "Final", "InitVar", "ReadOnly", "Required", "NotRequired", "Annotated", "Mapped", "WriteOnlyMapped", "DynamicMapped", // SQLAlchemy "typing.ClassVar", "typing.Final", "typing.Annotated", "typing.Required", "typing.NotRequired", "typing.ReadOnly", "typing_extensions.ClassVar", "typing_extensions.Final", "typing_extensions.Annotated", "typing_extensions.Required", "typing_extensions.NotRequired", "typing_extensions.ReadOnly", "dataclasses.InitVar", NULL}; for (int wi = 0; wrapper_names[wi]; wi++) { if (strcmp(btrim, wrapper_names[wi]) == 0) { if (arg_types && arg_n >= 1 && arg_types[0]) { return arg_types[0]; } } } // Type[T] / type[T] -> instance of T's metaclass... for // resolution purposes, treat as the class itself. if (strcmp(btrim, "Type") == 0 || strcmp(btrim, "type") == 0 || strcmp(btrim, "typing.Type") == 0) { if (arg_types && arg_n >= 1 && arg_types[0]) { return arg_types[0]; } } if (arg_types && arg_n > 0) { return cbm_type_template(arena, btrim, arg_types, arg_n); } return py_parse_type_text_qn(arena, btrim, module_qn); } } } { int cap = 4; const char **out = (const char **)cbm_arena_alloc(arena, (size_t)(cap + 1) * sizeof(const char *)); if (out) { int onum = 0; int d2 = 0; size_t start = 0; bool seen_pipe = false; for (size_t j = 0; j <= len; j++) { char cc = (j < len) ? ann[j] : '|'; if (cc == '[' || cc == '(' || cc == '{') d2++; else if (cc == ']' || cc == ')' || cc == '}') d2--; else if (cc == '|' && d2 == 0) { seen_pipe = (j < len); char *p = py_trim_ws(arena, ann + start, j - start); if (p && p[0] && onum < cap) out[onum++] = p; start = j + 1; } } if (seen_pipe && onum >= 2) { const CBMType **members = (const CBMType **)cbm_arena_alloc( arena, (size_t)(onum + 1) * sizeof(const CBMType *)); if (members) { for (int j = 0; j < onum; j++) { members[j] = py_parse_type_text_qn(arena, out[j], module_qn); } return cbm_type_union(arena, members, onum); } } } } static const char *builtins[] = {"int", "str", "bool", "float", "bytes", "None", "complex", "bytearray", "object", "type", NULL}; for (int i = 0; builtins[i]; i++) { if (strcmp(ann, builtins[i]) == 0) { return cbm_type_builtin(arena, ann); } } // Bare unqualified name: qualify with module_qn if it doesn't already // contain a dot. Skips obvious typing-module names so `Optional` / // `Self` / etc. don't end up qualified to the consumer's module. if (module_qn && !strchr(ann, '.')) { static const char *typing_names[] = {"Self", "Any", "None", "True", "False", "Iterator", "Iterable", "Generator", "AsyncIterator", "AsyncIterable", "Sequence", "MutableSequence", "Mapping", "MutableMapping", "Collection", "Container", "Reversible", "Optional", "Union", "Callable", "Awaitable", "Coroutine", "TypeVar", "ParamSpec", NULL}; bool is_typing = false; for (int i = 0; typing_names[i]; i++) { if (strcmp(ann, typing_names[i]) == 0) { is_typing = true; break; } } if (!is_typing) { const char *qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, ann); return cbm_type_named(arena, qn); } } return cbm_type_named(arena, ann); } static const CBMType *py_parse_type_text(CBMArena *arena, const char *ann) { return py_parse_type_text_qn(arena, ann, NULL); } /* Resolve a type-annotation text into a CBMType. Tries: scope lookup * (for imports / type aliases), module-qualified lookup in the registry, * then falls back to a bare NAMED. Strips quoted forward-reference * wrappers like `"Foo"` and converts subscripted forms `list[Foo]` / * `Optional[Foo]` / `Union[A, B]` into TEMPLATE / UNION shapes so * subsequent code can reach into element types for comprehensions * etc. */ static const CBMType *py_resolve_annotation(PyLSPContext *ctx, const char *ann) { if (!ann || !ann[0]) return cbm_type_unknown(); // Strip outer quotes for forward references: `"Foo"` -> `Foo`. size_t len = strlen(ann); if (len >= 2 && (ann[0] == '"' || ann[0] == '\'') && ann[len - 1] == ann[0]) { char *unquoted = (char *)cbm_arena_alloc(ctx->arena, len - 1); if (unquoted) { memcpy(unquoted, ann + 1, len - 2); unquoted[len - 2] = '\0'; return py_resolve_annotation(ctx, unquoted); } } // Generic subscript: `list[Foo]` / `Optional[Foo]` / `Union[A, B]` / // `dict[K, V]`. Parse the base name + comma-split args at depth 0. const char *lb = strchr(ann, '['); if (lb && len > 0 && ann[len - 1] == ']') { size_t base_len = (size_t)(lb - ann); char *base = (char *)cbm_arena_alloc(ctx->arena, base_len + 1); if (base) { memcpy(base, ann, base_len); base[base_len] = '\0'; // Trim leading/trailing whitespace from base. char *btrim = base; while (*btrim == ' ') btrim++; size_t blen = strlen(btrim); while (blen > 0 && btrim[blen - 1] == ' ') { btrim[blen - 1] = '\0'; blen--; } // Args text: between [ and ] size_t inner_start = (size_t)(lb - ann) + 1; size_t inner_len = len - inner_start - 1; char *args_text = (char *)cbm_arena_alloc(ctx->arena, inner_len + 1); if (args_text) { memcpy(args_text, ann + inner_start, inner_len); args_text[inner_len] = '\0'; int arg_n = 0; const char **arg_strs = py_split_subscript_args(ctx->arena, args_text, &arg_n); // Recursively resolve each arg to a CBMType. const CBMType **arg_types = NULL; if (arg_n > 0 && arg_strs) { arg_types = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(arg_n + 1) * sizeof(const CBMType *)); if (arg_types) { for (int i = 0; i < arg_n; i++) { arg_types[i] = py_resolve_annotation(ctx, arg_strs[i]); } arg_types[arg_n] = NULL; } } // Optional[X] -> UNION(X, None) if (strcmp(btrim, "Optional") == 0 || strcmp(btrim, "typing.Optional") == 0) { if (arg_types && arg_n >= 1 && arg_types[0]) { return cbm_type_optional(ctx->arena, arg_types[0]); } } // Union[A, B, ...] -> UNION if (strcmp(btrim, "Union") == 0 || strcmp(btrim, "typing.Union") == 0) { if (arg_types && arg_n > 0) { return cbm_type_union(ctx->arena, arg_types, arg_n); } } // Type wrappers that don't change the underlying type: // ClassVar / Final / InitVar / ReadOnly / Required / // NotRequired / Annotated / Mapped (SQLAlchemy) / Type[T]. // Returns the wrapped type T directly. static const char *wrapper_names[] = {"ClassVar", "Final", "InitVar", "ReadOnly", "Required", "NotRequired", "Annotated", "Mapped", "WriteOnlyMapped", "DynamicMapped", "Type", "type", "typing.ClassVar", "typing.Final", "typing.Annotated", "typing.Required", "typing.NotRequired", "typing.ReadOnly", "typing.Type", "typing_extensions.ClassVar", "typing_extensions.Final", "typing_extensions.Annotated", "typing_extensions.Required", "typing_extensions.NotRequired", "typing_extensions.ReadOnly", "dataclasses.InitVar", NULL}; for (int wi = 0; wrapper_names[wi]; wi++) { if (strcmp(btrim, wrapper_names[wi]) == 0) { if (arg_types && arg_n >= 1 && arg_types[0]) { return arg_types[0]; } } } // Generic containers -> TEMPLATE if (arg_types && arg_n > 0) { return cbm_type_template(ctx->arena, btrim, arg_types, arg_n); } return py_resolve_annotation(ctx, btrim); } } } // X | Y top-level union (PEP 604). { int depth = 0; for (size_t i = 0; i < len; i++) { char c = ann[i]; if (c == '[' || c == '(' || c == '{') depth++; else if (c == ']' || c == ')' || c == '}') depth--; else if (c == '|' && depth == 0) { int n = 0; const char **parts = py_split_subscript_args(ctx->arena, ann, &n); // py_split splits on commas — manually split on '|' at depth 0 (void)parts; (void)n; // Simpler: walk again splitting on '|'. int cap = 4; const char **out = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(cap + 1) * sizeof(const char *)); if (!out) break; int onum = 0; int d2 = 0; size_t start = 0; for (size_t j = 0; j <= len; j++) { char cc = (j < len) ? ann[j] : '|'; if (cc == '[' || cc == '(' || cc == '{') d2++; else if (cc == ']' || cc == ')' || cc == '}') d2--; else if (cc == '|' && d2 == 0) { char *p = py_trim_ws(ctx->arena, ann + start, j - start); if (p && p[0] && onum < cap) out[onum++] = p; start = j + 1; } } if (onum >= 2) { const CBMType **members = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(onum + 1) * sizeof(const CBMType *)); if (!members) break; for (int j = 0; j < onum; j++) { members[j] = py_resolve_annotation(ctx, out[j]); } return cbm_type_union(ctx->arena, members, onum); } break; } } } const CBMType *t = cbm_scope_lookup(ctx->current_scope, ann); if (!cbm_type_is_unknown(t)) return t; if (ctx->module_qn) { const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, ann); const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, qn); if (rt) return cbm_type_named(ctx->arena, qn); } // Common builtin names go to BUILTIN. static const char *builtins[] = {"int", "str", "bool", "float", "bytes", "None", "complex", "bytearray", "object", "type", NULL}; for (int i = 0; builtins[i]; i++) { if (strcmp(ann, builtins[i]) == 0) { return cbm_type_builtin(ctx->arena, ann); } } return cbm_type_named(ctx->arena, ann); } static void py_bind_parameters(PyLSPContext *ctx, TSNode params) { 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); const char *pk = ts_node_type(p); TSNode ident_node = {0}; TSNode type_node = {0}; if (strcmp(pk, "identifier") == 0) { ident_node = p; } else if (strcmp(pk, "typed_parameter") == 0 || strcmp(pk, "typed_default_parameter") == 0) { if (ts_node_named_child_count(p) > 0) ident_node = ts_node_named_child(p, 0); type_node = ts_node_child_by_field_name(p, "type", 4); } else if (strcmp(pk, "default_parameter") == 0) { ident_node = ts_node_child_by_field_name(p, "name", 4); } else if (strcmp(pk, "list_splat_pattern") == 0 || strcmp(pk, "dictionary_splat_pattern") == 0) { if (ts_node_named_child_count(p) > 0) ident_node = ts_node_named_child(p, 0); } else if (strcmp(pk, "typed_default_parameter") == 0) { // already covered above by typed_parameter branch } // Splat-typed parameter: `*args: int` -> args is tuple[int, ...] // `**kwargs: V` -> kwargs is dict[str, V]. Tree-sitter Python // wraps these in typed_parameter where the inner is // list_splat_pattern / dictionary_splat_pattern. Handle here. bool is_splat = false; bool is_kwargs = false; if (strcmp(pk, "typed_parameter") == 0 && ts_node_named_child_count(p) > 0) { TSNode first = ts_node_named_child(p, 0); const char *fk = ts_node_type(first); if (strcmp(fk, "list_splat_pattern") == 0) { is_splat = true; if (ts_node_named_child_count(first) > 0) ident_node = ts_node_named_child(first, 0); } else if (strcmp(fk, "dictionary_splat_pattern") == 0) { is_kwargs = true; if (ts_node_named_child_count(first) > 0) ident_node = ts_node_named_child(first, 0); } } if (ts_node_is_null(ident_node)) continue; char *name = py_node_text(ctx, ident_node); if (!name) continue; const CBMType *t = cbm_type_unknown(); if (!ts_node_is_null(type_node)) { char *ann = py_node_text(ctx, type_node); t = py_resolve_annotation(ctx, ann); } // Splat parameter wrapping: *args: T -> tuple[T, ...]; **kwargs: T // -> dict[str, T]. The annotation gives the element type T; we // wrap it in the appropriate container. if (is_splat && t && !cbm_type_is_unknown(t)) { t = cbm_type_template(ctx->arena, "tuple", &t, 1); } if (is_kwargs && t && !cbm_type_is_unknown(t)) { const CBMType *str_t = cbm_type_builtin(ctx->arena, "str"); const CBMType *args[3] = {str_t, t, NULL}; t = cbm_type_template(ctx->arena, "dict", args, 2); } py_scope_bind(ctx, name, t); } } static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char *container_qn) { TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4); if (ts_node_is_null(name_node)) return; char *fname = py_node_text(ctx, name_node); if (!fname || !fname[0]) return; const char *prev_func = ctx->enclosing_func_qn; const char *base_qn = container_qn ? container_qn : ctx->module_qn; ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", base_qn, fname); CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); TSNode params = ts_node_child_by_field_name(func_node, "parameters", 10); py_bind_parameters(ctx, params); // For methods, bind `self`/`cls` AFTER param walk so the receiver type // wins over the unannotated `self` / `cls` parameter declaration. if (ctx->enclosing_class_qn) { py_scope_bind(ctx, "self", cbm_type_named(ctx->arena, ctx->enclosing_class_qn)); py_scope_bind(ctx, "cls", cbm_type_named(ctx->arena, ctx->enclosing_class_qn)); } TSNode body = ts_node_child_by_field_name(func_node, "body", 4); if (!ts_node_is_null(body)) { py_resolve_calls_in(ctx, body); // Also descend into nested function/class definitions in the body. uint32_t bnc = ts_node_named_child_count(body); for (uint32_t i = 0; i < bnc; i++) { TSNode c = ts_node_named_child(body, i); const char *ck = ts_node_type(c); if (strcmp(ck, "function_definition") == 0) { py_process_function(ctx, c, ctx->enclosing_func_qn); } else if (strcmp(ck, "decorated_definition") == 0) { TSNode def = ts_node_child_by_field_name(c, "definition", 10); if (!ts_node_is_null(def) && strcmp(ts_node_type(def), "function_definition") == 0) { py_process_function(ctx, def, ctx->enclosing_func_qn); } } } } py_scope_restore(ctx, saved); ctx->enclosing_func_qn = prev_func; } /* Return true iff func_node's name is __init__ or __post_init__. */ static bool py_is_init_method(PyLSPContext *ctx, TSNode func_node) { TSNode name = ts_node_child_by_field_name(func_node, "name", 4); if (ts_node_is_null(name)) return false; char *nm = py_node_text(ctx, name); return nm && (strcmp(nm, "__init__") == 0 || strcmp(nm, "__post_init__") == 0); } static void py_process_class(PyLSPContext *ctx, TSNode class_node) { TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4); if (ts_node_is_null(name_node)) return; char *cname = py_node_text(ctx, name_node); if (!cname || !cname[0]) return; const char *prev_class = ctx->enclosing_class_qn; ctx->enclosing_class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); TSNode body = ts_node_child_by_field_name(class_node, "body", 4); if (!ts_node_is_null(body)) { uint32_t bnc = ts_node_named_child_count(body); // First pass: process class-level annotated assignments (PEP 526 // class-body field annotations like `x: int`) and dunder __init__ // methods so fields are registered before sibling methods that // reference them. for (uint32_t i = 0; i < bnc; i++) { TSNode c = ts_node_named_child(body, i); const char *ck = ts_node_type(c); if (strcmp(ck, "expression_statement") == 0 && ts_node_named_child_count(c) > 0) { TSNode inner = ts_node_named_child(c, 0); const char *ik = ts_node_type(inner); // class C: x: int = 1 → bind x as field if (strcmp(ik, "assignment") == 0) { TSNode left = ts_node_child_by_field_name(inner, "left", 4); TSNode ann = ts_node_child_by_field_name(inner, "type", 4); if (!ts_node_is_null(left) && !ts_node_is_null(ann) && strcmp(ts_node_type(left), "identifier") == 0) { char *fname = py_node_text(ctx, left); char *atext = py_node_text(ctx, ann); if (fname && atext) { const CBMType *ft = py_resolve_annotation(ctx, atext); py_register_instance_field(ctx, ctx->enclosing_class_qn, fname, ft); } } } } } // Second pass: __init__ (so self.x assignments populate fields). for (uint32_t i = 0; i < bnc; i++) { TSNode c = ts_node_named_child(body, i); const char *ck = ts_node_type(c); TSNode fn_node = c; bool is_decorated = strcmp(ck, "decorated_definition") == 0; if (is_decorated) { fn_node = ts_node_child_by_field_name(c, "definition", 10); if (ts_node_is_null(fn_node) || strcmp(ts_node_type(fn_node), "function_definition") != 0) continue; } else if (strcmp(ck, "function_definition") != 0) { continue; } if (py_is_init_method(ctx, fn_node)) { py_process_function(ctx, fn_node, ctx->enclosing_class_qn); } } // Third pass: every other method (and nested classes). for (uint32_t i = 0; i < bnc; i++) { TSNode c = ts_node_named_child(body, i); const char *ck = ts_node_type(c); if (strcmp(ck, "function_definition") == 0) { if (!py_is_init_method(ctx, c)) { py_process_function(ctx, c, ctx->enclosing_class_qn); } } else if (strcmp(ck, "decorated_definition") == 0) { TSNode def = ts_node_child_by_field_name(c, "definition", 10); if (!ts_node_is_null(def) && strcmp(ts_node_type(def), "function_definition") == 0 && !py_is_init_method(ctx, def)) { py_process_function(ctx, def, ctx->enclosing_class_qn); } } else if (strcmp(ck, "class_definition") == 0) { py_process_class(ctx, c); } } } ctx->enclosing_class_qn = prev_class; } /* Bind every Class / Type definition from the registry into the root scope * as NAMED(qn). Lets bare references like `Foo()` and `c: Foo` resolve to * the registered class type. */ static void py_bind_module_classes(PyLSPContext *ctx) { if (!ctx || !ctx->registry || !ctx->module_qn) return; const CBMRegisteredType *types = ctx->registry->types; int n = ctx->registry->type_count; size_t prefix_len = strlen(ctx->module_qn); for (int i = 0; i < n; i++) { const char *qn = types[i].qualified_name; const char *sname = types[i].short_name; if (!qn || !sname) continue; if (strncmp(qn, ctx->module_qn, prefix_len) != 0) continue; if (qn[prefix_len] != '.') continue; py_scope_bind(ctx, sname, cbm_type_named(ctx->arena, qn)); } } void py_lsp_process_file(PyLSPContext *ctx, TSNode root) { if (!ctx || ts_node_is_null(root)) return; py_lsp_bind_imports(ctx); py_bind_module_classes(ctx); uint32_t nc = ts_node_named_child_count(root); // Pass 1: top-level assignments bind into module scope. for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(root, i); py_process_statement(ctx, c); } // Pass 2: top-level calls (rare) and nested definitions. const char *prev_func = ctx->enclosing_func_qn; ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.__module__", ctx->module_qn); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(root, i); const char *ck = ts_node_type(c); if (strcmp(ck, "function_definition") == 0) { py_process_function(ctx, c, NULL); } else if (strcmp(ck, "class_definition") == 0) { py_process_class(ctx, c); } else if (strcmp(ck, "decorated_definition") == 0) { TSNode def = ts_node_child_by_field_name(c, "definition", 10); if (ts_node_is_null(def)) continue; const char *dk = ts_node_type(def); if (strcmp(dk, "function_definition") == 0) { py_process_function(ctx, def, NULL); } else if (strcmp(dk, "class_definition") == 0) { py_process_class(ctx, def); } } else if (strcmp(ck, "expression_statement") == 0) { // Top-level call statements py_resolve_calls_in(ctx, c); } } ctx->enclosing_func_qn = prev_func; } /* Register one definition into the registry. Returns true if recognized. */ static bool py_register_def(CBMArena *arena, CBMTypeRegistry *reg, CBMDefinition *d, const char *module_qn) { if (!d || !d->qualified_name || !d->name || !d->label) return false; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->name; rt.is_interface = false; if (d->base_classes) { // Python's extract_defs records the entire `(Base, Other)` argument // list as a single base_classes[0] entry. Split commas and strip // parens / whitespace / generic subscripts to recover usable QNs. int cap = 8; const char **embedded = (const char **)cbm_arena_alloc(arena, (size_t)(cap + 1) * sizeof(const char *)); int n = 0; for (int j = 0; d->base_classes[j]; j++) { const char *raw = d->base_classes[j]; size_t raw_len = strlen(raw); // Trim outer parens if present. size_t lo = 0; size_t hi = raw_len; while (lo < hi && (raw[lo] == '(' || raw[lo] == ' ')) lo++; while (hi > lo && (raw[hi - 1] == ')' || raw[hi - 1] == ' ')) hi--; // Split on commas at depth 0 (ignore commas inside [], ()). size_t start = lo; int depth = 0; for (size_t k = lo; k <= hi; k++) { char c = (k < hi) ? raw[k] : ','; if (c == '[' || c == '(') depth++; else if (c == ']' || c == ')') depth--; else if (c == ',' && depth == 0) { size_t s = start; size_t e = k; while (s < e && raw[s] == ' ') s++; while (e > s && raw[e - 1] == ' ') e--; // Strip generic subscript: "Foo[T]" -> "Foo" size_t lb = s; while (lb < e && raw[lb] != '[') lb++; size_t name_end = lb; // Skip keyword args like "metaclass=Meta" (contains '=') size_t eq = s; while (eq < name_end && raw[eq] != '=') eq++; if (eq >= name_end && name_end > s) { size_t blen = name_end - s; char *bname = (char *)cbm_arena_alloc(arena, blen + 1); if (bname) { memcpy(bname, raw + s, blen); bname[blen] = '\0'; if (n >= cap) { int new_cap = cap * 2; const char **grown = (const char **)cbm_arena_alloc( arena, (size_t)(new_cap + 1) * sizeof(const char *)); if (grown) { for (int q = 0; q < n; q++) grown[q] = embedded[q]; embedded = grown; cap = new_cap; } } if (n < cap) { embedded[n++] = strchr(bname, '.') ? bname : cbm_arena_sprintf(arena, "%s.%s", module_qn, bname); } } } start = k + 1; } } } embedded[n] = NULL; if (n > 0) rt.embedded_types = embedded; } cbm_registry_add_type(reg, rt); return true; } if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = d->qualified_name; rf.short_name = d->name; // Translate Python decorators into flags + carry the QN list. if (d->decorators) { int dec_count = 0; while (d->decorators[dec_count]) dec_count++; if (dec_count > 0) { const char **dec_qns = (const char **)cbm_arena_alloc( arena, (size_t)(dec_count + 1) * sizeof(const char *)); for (int j = 0; j < dec_count; j++) { const char *dec = d->decorators[j]; dec_qns[j] = cbm_arena_strdup(arena, dec); // Match by short name (last "." segment) so both // "property" and "functools.cache" / "abc.abstractmethod" // forms light up. const char *short_dec = strrchr(dec, '.'); short_dec = short_dec ? short_dec + 1 : dec; if (strcmp(short_dec, "property") == 0) rf.flags |= CBM_FUNC_FLAG_PROPERTY; else if (strcmp(short_dec, "classmethod") == 0) rf.flags |= CBM_FUNC_FLAG_CLASSMETHOD; else if (strcmp(short_dec, "staticmethod") == 0) rf.flags |= CBM_FUNC_FLAG_STATICMETHOD; else if (strcmp(short_dec, "abstractmethod") == 0) rf.flags |= CBM_FUNC_FLAG_ABSTRACTMETHOD; else if (strcmp(short_dec, "overload") == 0) rf.flags |= CBM_FUNC_FLAG_OVERLOAD; else if (strcmp(short_dec, "final") == 0) rf.flags |= CBM_FUNC_FLAG_FINAL; } dec_qns[dec_count] = NULL; rf.decorator_qns = dec_qns; } } const CBMType **ret_types = NULL; // Prefer d->return_type (full text) when it has subscript brackets // — extract_defs.c::extract_return_types strips them in its array // form, which loses subscript args like Optional[Foo]. d->return_type // is the cbm_node_text of the whole annotation node, which is the // form py_parse_type_text_qn expects. bool prefer_full = d->return_type && d->return_type[0] && strchr(d->return_type, '['); if (prefer_full) { ret_types = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); ret_types[0] = py_parse_type_text_qn(arena, d->return_type, module_qn); ret_types[1] = NULL; } else if (d->return_types && d->return_types[0]) { int count = 0; while (d->return_types[count]) count++; ret_types = (const CBMType **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const CBMType *)); for (int j = 0; j < count; j++) { ret_types[j] = py_parse_type_text_qn(arena, d->return_types[j], module_qn); } ret_types[count] = NULL; } else if (d->return_type && d->return_type[0]) { ret_types = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); ret_types[0] = py_parse_type_text_qn(arena, d->return_type, module_qn); ret_types[1] = NULL; } rf.signature = cbm_type_func(arena, d->param_names, NULL, ret_types); if (strcmp(d->label, "Method") == 0) { // Receiver type: the enclosing class. Python def metadata may // record the class name in `receiver` or via the QN itself. const char *qn = d->qualified_name; const char *last_dot = strrchr(qn, '.'); if (last_dot && last_dot != qn) { size_t prefix_len = (size_t)(last_dot - qn); rf.receiver_type = cbm_arena_strndup(arena, qn, prefix_len); } } cbm_registry_add_func(reg, rf); return true; } return false; } /* ── cbm_run_py_lsp: single-file entry point ──────────────────── */ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, TSNode root) { if (!arena || !result) return; /* Inject minimal builtin definitions as real graph nodes (builtins.len, * builtins.str, builtins.str.upper, ...). The typeshed registry already * RESOLVES builtin calls (emitting the strategy + a "builtins.*" callee_qn), * but pass_calls.c only writes the CALLS edge when that callee_qn maps to a * graph node. We run inside cbm_extract_file, before the pipeline mints * def nodes from result->defs, so these become the target nodes the * builtin/constructor/method edges point at. Upsert dedups by QN. */ py_builtins_inject_defs(result, arena); CBMTypeRegistry reg; cbm_registry_init(®, arena); cbm_python_stdlib_register(®, arena); const char *module_qn = result->module_qn; // Register the file's own definitions so calls inside this file can // resolve via the registry. for (int i = 0; i < result->defs.count; i++) { py_register_def(arena, ®, &result->defs.items[i], module_qn); } PyLSPContext ctx; py_lsp_init(&ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls); /* Let the resolver inject synthetic syntactic calls for operator/subscript * dunder desugaring so those recovered calls reach the CALLS-edge pipeline. */ ctx.syn_calls = &result->calls; for (int i = 0; i < result->imports.count; i++) { CBMImport *imp = &result->imports.items[i]; if (imp->local_name && imp->module_path) { py_lsp_add_import(&ctx, imp->local_name, imp->module_path); } } py_lsp_process_file(&ctx, root); } /* ── Cross-file + batch ───────────────────────────────────────── */ extern const TSLanguage *tree_sitter_python(void); /* Split a "|"-separated list into a NULL-terminated array of arena copies. */ static const char **py_split_pipe(CBMArena *arena, const char *text) { if (!text || !text[0]) return NULL; int count = 1; for (const char *p = text; *p; p++) if (*p == '|') count++; const char **out = (const char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const char *)); if (!out) return NULL; int idx = 0; const char *start = text; for (const char *p = text;; p++) { if (*p == '|' || *p == '\0') { size_t n = (size_t)(p - start); char *s = (char *)cbm_arena_alloc(arena, n + 1); if (!s) return NULL; memcpy(s, start, n); s[n] = '\0'; out[idx++] = s; if (*p == '\0') break; start = p + 1; } } out[idx] = NULL; return out; } /* Build a registry from CBMLSPDef[] supplied by the caller — covers both * the source file's own defs and cross-file referenced defs. */ static void py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg, CBMLSPDef *defs, int def_count) { /* Pass 1: types only — the method pass probes the registry per Method def * (receiver auto-registration), which is a LINEAR scan pre-finalize: * O(methods x types) per file (same quadratic as php_register_lsp_defs; * see that function's comment). Types first, finalize, then methods. */ for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; if (!d->qualified_name || !d->short_name || !d->label) continue; if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Protocol") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; /* borrowed — d outlives this call */ rt.short_name = d->short_name; rt.is_interface = d->is_interface || strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Protocol") == 0; rt.embedded_types = py_split_pipe(arena, d->embedded_types); if (d->method_names_str && d->method_names_str[0]) { rt.method_names = py_split_pipe(arena, d->method_names_str); } cbm_registry_add_type(reg, rt); } } /* idx_arena == NULL skips the mid-build finalize: the tier-2 builder * registers one def per call — finalizing per def would rebuild the * buckets def_count times and leak each generation into the shared * registry arena. */ if (idx_arena) { cbm_registry_finalize_into(reg, idx_arena); } /* Pass 2: functions and methods. */ for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; if (!d->qualified_name || !d->short_name || !d->label) continue; if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = d->qualified_name; /* borrowed */ rf.short_name = d->short_name; // Build FUNC type from "|"-separated return types. const char **ret_strs = py_split_pipe(arena, d->return_types); const CBMType **ret_types = NULL; if (ret_strs) { int n = 0; while (ret_strs[n]) n++; if (n > 0) { ret_types = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(const CBMType *)); for (int j = 0; j < n; j++) { ret_types[j] = cbm_type_named(arena, ret_strs[j]); } ret_types[n] = NULL; } } rf.signature = cbm_type_func(arena, NULL, NULL, ret_types); if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) { rf.receiver_type = d->receiver_type; /* borrowed */ if (!cbm_registry_lookup_type(reg, rf.receiver_type)) { CBMRegisteredType auto_t; memset(&auto_t, 0, sizeof(auto_t)); auto_t.qualified_name = rf.receiver_type; const char *dot = strrchr(d->receiver_type, '.'); auto_t.short_name = dot ? dot + 1 : rf.receiver_type; /* borrowed substring */ cbm_registry_add_type(reg, auto_t); } } cbm_registry_add_func(reg, rf); } } } void cbm_run_py_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 || source_len <= 0 || !out) return; TSParser *parser = NULL; TSTree *tree = cached_tree; bool owns_tree = false; if (!tree) { parser = ts_parser_new(); if (!parser) return; ts_parser_set_language(parser, tree_sitter_python()); tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len); owns_tree = true; if (!tree) { ts_parser_delete(parser); return; } } TSNode root = ts_tree_root_node(tree); CBMTypeRegistry reg; cbm_registry_init(®, arena); cbm_python_stdlib_register(®, arena); /* per-file path: defs[] already filtered by caller, no lang-check needed */ /* Index allocations go to a per-call scratch arena (see php_lsp_cross). */ CBMArena idx_arena; cbm_arena_init(&idx_arena); py_register_lsp_defs(arena, &idx_arena, ®, defs, def_count); /* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize" * comment for the rationale. */ cbm_registry_finalize_into(®, &idx_arena); PyLSPContext ctx; py_lsp_init(&ctx, arena, source, source_len, ®, module_qn, out); for (int i = 0; i < import_count; i++) { if (import_names && import_qns && import_names[i] && import_qns[i]) { py_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } py_lsp_process_file(&ctx, root); cbm_arena_destroy(&idx_arena); if (owns_tree && tree) ts_tree_delete(tree); if (parser) ts_parser_delete(parser); } /* ── Tier 2: pre-built per-language registry (mirrors Go pilot) ── */ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) { if (!arena) return NULL; CBMTypeRegistry *reg = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*reg)); if (!reg) return NULL; cbm_registry_init(reg, arena); cbm_python_stdlib_register(reg, arena); /* Filter to Python defs only — defs[] is mixed-language all_defs. */ for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; if (d->lang != CBM_LANG_PYTHON) continue; /* Reuse the existing register fn on a single-def slice (n=1 inline). */ py_register_lsp_defs(arena, NULL, reg, d, 1); } cbm_registry_finalize(reg); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; } void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMTypeRegistry *reg, const char **import_names, const char **import_qns, int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!arena || !source || source_len <= 0 || !out || !reg) return; TSParser *parser = NULL; TSTree *tree = cached_tree; bool owns_tree = false; if (!tree) { parser = ts_parser_new(); if (!parser) return; ts_parser_set_language(parser, tree_sitter_python()); tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len); owns_tree = true; if (!tree) { ts_parser_delete(parser); return; } } TSNode root = ts_tree_root_node(tree); PyLSPContext ctx; py_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out); for (int i = 0; i < import_count; i++) { if (import_names && import_qns && import_names[i] && import_qns[i]) { py_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } py_lsp_process_file(&ctx, root); if (owns_tree && tree) ts_tree_delete(tree); if (parser) ts_parser_delete(parser); } void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count, CBMResolvedCallArray *out) { if (!arena || !files || file_count <= 0 || !out) return; for (int f = 0; f < file_count; f++) { CBMBatchPyLSPFile *file = &files[f]; memset(&out[f], 0, sizeof(CBMResolvedCallArray)); if (!file->source || file->source_len <= 0) continue; CBMArena file_arena; cbm_arena_init(&file_arena); CBMResolvedCallArray file_out; memset(&file_out, 0, sizeof(file_out)); cbm_run_py_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn, file->defs, file->def_count, file->import_names, file->import_qns, file->import_count, file->cached_tree, &file_out); if (file_out.count > 0) { out[f].count = file_out.count; out[f].items = (CBMResolvedCall *)cbm_arena_alloc(arena, (size_t)file_out.count * sizeof(CBMResolvedCall)); if (out[f].items) { for (int j = 0; j < file_out.count; j++) { CBMResolvedCall *src = &file_out.items[j]; CBMResolvedCall *dst = &out[f].items[j]; dst->caller_qn = src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL; dst->callee_qn = src->callee_qn ? cbm_arena_strdup(arena, src->callee_qn) : NULL; dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL; dst->confidence = src->confidence; dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL; } } else { out[f].count = 0; } } cbm_arena_destroy(&file_arena); } } /* ── Stdlib stub — Phase 10 replaces with auto-generated body ─── */ #ifndef CBM_PYTHON_STDLIB_GENERATED void cbm_python_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) { (void)reg; (void)arena; } #endif