#ifndef CBM_LSP_PY_LSP_H #define CBM_LSP_PY_LSP_H #include "type_rep.h" #include "scope.h" #include "type_registry.h" #include "../cbm.h" #include "go_lsp.h" // CBMLSPDef, CBMResolvedCallArray reused across languages // Lambda body record. When a lambda is assigned to a name (`fn = // lambda x: x.method()`), we stash its parameter list + body so the // next call site (`fn(arg)`) can do call-site driven inference. typedef struct { const char *name; // bound name, e.g. "fn" TSNode lambda_node; // the lambda AST node } CBMLambdaEntry; // Function-as-dict-value entry. When `funcs = {"a": foo, "b": bar}` is // seen, we record the per-key target QN so `funcs["a"]()` emits an // edge to foo. typedef struct { const char *var_name; // e.g. "funcs" const char *literal_key; // e.g. "a" const char *target_qn; // e.g. "test.main.foo" } CBMDictLiteralEntry; // Memoization entry for py_eval_expr_type (issue #710). Keyed by the // tree-sitter node identity pointer (TSNode.id), which is unique per node // within one file's parse tree. NOT the start byte: every leftmost // descendant of a chained call expression shares the chain's start byte, // so byte-keyed entries would alias distinct nodes and silently corrupt // call resolution. `gen` snapshots the scope generation (see // py_scope_bind in py_lsp.c); entries from older generations are treated // as misses so scope rebinding can never serve a stale type. typedef struct { const void *node_id; // TSNode.id; NULL marks an empty slot uint32_t gen; // scope generation at insert time const CBMType *result; // full-fidelity result, never NULL in a live entry } CBMPyTypeCacheEntry; // PyLSPContext holds state for one Python file's type evaluation. typedef struct { CBMArena *arena; const char *source; int source_len; const CBMTypeRegistry *registry; CBMScope *current_scope; // Import map: local_name -> module_qn (arena-allocated, NULL-terminated). // Built from CBMFileResult.imports. const char **import_local_names; const char **import_module_qns; int import_count; // Current function/class context for resolving `self`/`cls` and emitting // caller QNs. const char *enclosing_func_qn; const char *enclosing_class_qn; const char *module_qn; // Output: resolved calls accumulate here. CBMResolvedCallArray *resolved_calls; // Syntactic-call list (result->calls), borrowed from the per-file // extraction result. The downstream pipeline only turns a resolved_call // into a CALLS edge when a *syntactic* CBMCall with the same // (enclosing_func_qn, callee short-name) exists here. Operator/subscript // dunder desugaring (`s[k]` -> T.__getitem__, `a + b` -> T.__add__) never // appears in result->calls because a `subscript`/`binary_operator` is not // a `call` node, so the syntactic extractor produced no call. When this is // non-NULL, py_emit_dunder_call injects a matching synthetic CBMCall so the // recovered dunder call becomes a real CALLS edge. NULL in the cross-file // path (no result->calls available there). Mirrors RustLSPContext.syn_calls. CBMCallArray *syn_calls; // Lambda registry: simple linear list, looked up by name. CBMLambdaEntry *lambdas; int lambda_count; int lambda_cap; // Dict-literal-as-dispatch-table tracking. CBMDictLiteralEntry *dict_literals; int dict_literal_count; int dict_literal_cap; // AST-walk recursion depth for py_resolve_calls_in (guards stack overflow on // deeply-nested/cyclic files; see cbm_lsp_max_walk_depth). Zero via memset. int walk_depth; // py_eval_expr_type memoization + guards (issues #710/#720; mirrors // c_eval_expr_type's guard design in c_lsp.c). All zero via memset — // each file starts with a cold cache and a full budget. CBMPyTypeCacheEntry *type_cache; // open addressing, linear probe, arena-allocated int type_cache_count; // occupied slots (kept < 75% of cap) int type_cache_cap; // power-of-two capacity uint32_t type_cache_gen; // bumped on every scope mutation (O(1) flush) int eval_depth; // evaluator recursion depth (PY_LSP_MAX_EVAL_DEPTH) int eval_steps; // per-file work budget used (PY_EVAL_MAX_STEPS_PER_FILE) uint32_t eval_truncations; // depth/budget cutoff count — gates memo inserts // Per-file instance-field OVERLAY. When the Tier-2 registry is shared + sealed // (registry->read_only), `self.x = ...` / PEP-526 field discoveries made during // resolve are recorded HERE instead of mutating the shared registry (which would // bypass the add_* seal, race the other resolve workers, and leave the shared // entry pointing into this file's arena once it is freed). py_lookup_field // consults this overlay alongside the shared base, so same-file attribute-chain // resolution is preserved with zero shared mutation. Arena-allocated (per-file // lifetime); holds no pointer into the shared registry, and the shared registry // holds none into it. Mutable per-file registries keep the direct write. struct { const char *class_qn; const char *field_name; const CBMType *field_type; } *field_overlay; int field_overlay_count; int field_overlay_cap; // Debug mode (CBM_LSP_DEBUG env, shared across all language LSPs). bool debug; } PyLSPContext; // Initialize a PyLSPContext for processing one file. void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int source_len, const CBMTypeRegistry *registry, const char *module_qn, CBMResolvedCallArray *out); // Add an import mapping (call once per CBMImport entry). void py_lsp_add_import(PyLSPContext *ctx, const char *local_name, const char *module_qn); // Bind every recorded import into the root scope. Idempotent. Called from // py_lsp_process_file before AST traversal. Exposed for unit tests that // don't need a tree-sitter parse. void py_lsp_bind_imports(PyLSPContext *ctx); // Test hook: lookup a name in the context's scope chain. Returns // cbm_type_unknown() if not bound. const CBMType *py_lsp_lookup_in_scope(const PyLSPContext *ctx, const char *name); // Process all functions/classes in a file's AST, evaluating types and resolving calls. // root must be the tree-sitter Python `module` node. void py_lsp_process_file(PyLSPContext *ctx, TSNode root); // Entry point: build registry from file's own defs + run LSP resolution. // Mirrors cbm_run_go_lsp / cbm_run_c_lsp. void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, TSNode root); // Register Python stdlib types and functions into a registry. // Auto-generated by scripts/gen-py-stdlib.py from typeshed. // Phase 10 fills the body; Phase 2 ships a no-op so linkage works. void cbm_python_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena); // --- Cross-file LSP resolution --- 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, // NULL = parse internally CBMResolvedCallArray *out); /* Tier 2: pre-built per-language registry (mirrors Go pilot). * Filters all_defs to Python entries, builds + finalizes once. */ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count); void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY const char **import_names, const char **import_qns, int import_count, TSTree *cached_tree, CBMResolvedCallArray *out); // --- Batch cross-file LSP --- typedef struct { const char *source; int source_len; const char *module_qn; TSTree *cached_tree; // from TSTree caching (NULL = parse internally) CBMLSPDef *defs; // combined file-local + cross-file defs int def_count; const char **import_names; // parallel arrays, import_count long const char **import_qns; int import_count; } CBMBatchPyLSPFile; void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count, CBMResolvedCallArray *out); #endif // CBM_LSP_PY_LSP_H