Files
codebase-memory-mcp/internal/cbm/lsp/py_lsp.h
T
Martin Vogel 39f41c3d47 feat(py_lsp): close all stress gaps — lambda, dict-dispatch, async, callable, next, kwargs
Round 10. Closes the last documented gaps from Round 9 plus 21 new
real-world stress tests covering SQLAlchemy / Pydantic / dataclass /
TypedDict / Protocol / generator / async patterns.

Lambda call-site inference:
- PyLSPContext gains a per-context lambda registry. `fn = lambda x:
  body` records the lambda's AST node bound to fn. At call site
  fn(arg), we push a sub-scope, bind lambda params from arg types,
  and walk the body. Edges emitted under a synthetic <lambda> child
  of the enclosing function so traces attribute correctly.

Dict-literal dispatch table:
- `funcs = {"a": foo, "b": bar}` records per-literal-key target QNs
  in CBMDictLiteralEntry[]. funcs["a"]() emits an edge directly to
  foo's QN with strategy=lsp_dict_dispatch.

Async with / async for:
- `async with X() as f` looks up __aenter__ before __enter__.
  __aenter__'s declared return type wins; the manager type is the
  fallback (async __aenter__s typically return self).
- `async for x in iter` falls back to __anext__'s return type when
  the iterable's TEMPLATE form isn't recognized as AsyncIterator /
  AsyncGenerator.

Callable / next / iter:
- `f()` where f resolves to CBM_TYPE_CALLABLE returns the callable's
  return type. TEMPLATE("Callable", [..., R]) returns R.
- factory()() / (lambda)() etc — when the function expression is
  itself a call / parenthesized / lambda, evaluate it and use its
  return type.
- `next(it)` returns py_iterable_element_type(type-of-it).
- `iter(x)` is identity.

*args / **kwargs typed-parameter wrapping:
- `def f(*args: T)` -> args is TEMPLATE("tuple", [T]).
- `def f(**kwargs: T)` -> kwargs is TEMPLATE("dict", [str, T]).
- Detected by walking typed_parameter > list_splat_pattern /
  dictionary_splat_pattern wrappers in py_bind_parameters.

Tuple / list / set / dict literal expressions:
- py_eval_expr_type now builds structured TUPLE / TEMPLATE types
  from element types of the literal. `(Foo(), Bar())` -> TUPLE([Foo,
  Bar]); `[Foo()]` -> TEMPLATE("list", [Foo]); `{"k": Foo()}` ->
  TEMPLATE("dict", [str, Foo]). Without this, unpacking lost element
  precision.

Match sequence pattern:
- list_pattern wraps each element in another case_pattern; capture
  appears as dotted_name -> identifier; splat as splat_pattern.
  Element-binder walks both wrappers and binds capture/splat to the
  subject's iterable element type / list[elem].

Stress test suite expanded from 22 to 43 patterns. All 43 hard-
asserted as PASS:
- typing helpers: NamedTuple, TypedDict (incl. total=False), Protocol
  (nominal + structural), ABC + abstractmethod, Mapped[T] (SQLAlchemy
  2.0), Pydantic-style BaseModel
- control flow: with-as via __enter__, except-as, post-early-return
  narrowing, async with via __aenter__, async for via __anext__
- containers: tuple unpack from function return + literal,
  dict.items() comprehension k/v unpack, list slice, dict.get
  default, recursive self-referencing types via forward refs
- decorators: factory + functools.wraps, classmethod chained to
  method, property setter
- inheritance: Self in chain, diamond MRO with C3, multi-class match
- functional: lambda call-site inference, function-as-dict-value
  dispatch, callable return-value, next(iter), generator delegation
  (yield from), iterator next, double-star **kwargs, isinstance |
  branches, module-level const, dataclass + default_factory

Bench: 178 LOC, 52 calls, **52 resolved (100%)**, 11.25 ms (ASan +
UBSan). Total 2941 / 0. Zero remaining KNOWN GAPs in the stress
suite — every probed pattern resolves.
2026-05-09 14:16:15 +02:00

125 lines
4.4 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;
// 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;
// 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;
// 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);
// --- 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