189 lines
8.6 KiB
C
189 lines
8.6 KiB
C
#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
|