Hybrid LSP Semantic Type Resolution in Python, TypeScript, Go, and Rust: A Deep Dive into codebase-memory-mCP
The Hybrid LSP layer in DeusData/codebase-memory-mCP performs type-aware semantic analysis by combining Tree-sitter's syntactic AST with language-specific C implementations that resolve imports, generics, and method dispatch across Python, TypeScript, Go, Rust, and other languages to produce accurate cross-file call graphs.
The codebase-memory-mCP project builds a full-text knowledge graph of codebases spanning 158 languages. While Tree-sitter provides fast syntactic parsing, it cannot resolve imports, infer generic types, or trace inheritance chains. To bridge this gap, the repository implements hybrid LSP semantic type resolution—a set of lightweight, in-process language servers written in C that embed directly into the indexing pipeline, eliminating external LSP process overhead while delivering IDE-level accuracy.
Why Tree-Sitter Alone Cannot Build Semantic Call Graphs
Tree-sitter generates accurate syntactic ASTs, but raw trees miss the semantic links that define program behavior. Imports, generic arguments, standard-library types, and method resolution through inheritance or traits remain invisible. Without binding user.profile.display_name() to the actual definition Profile.display_name, cross-file navigation fails and call graphs remain incomplete.
The Hybrid LSP Architecture
The system operates a dual-pass indexing strategy orchestrated in src/pipeline/pass_lsp_cross.c and parallelized via src/pipeline/pass_parallel.c.
- Tree-sitter pass – Fast extraction of definitions, imports, and call sites.
- Hybrid LSP pass – Per-language type-aware resolver that binds imports, infers generics, walks MROs (Method Resolution Order), substitutes
Selftypes, and emits resolved call edges.
All LSP modules are pure C and link into a static binary, running in-process with the tree-sitter parser. This design allows indexing the Linux kernel (28 M LOC) in under three minutes without spawning separate language server processes.
Language-Specific Implementation Details
Each language module lives in internal/cbm/lsp/ and shares common data structures defined in internal/cbm/cbm.h, including CBMType and CBMScope.
Python Resolution in py_lsp.c
The Python resolver (internal/cbm/lsp/py_lsp.c, ~3,200 lines) handles import binding, MRO traversal, and generic substitution.
Import binding occurs via py_lsp_bind_imports (lines 188-224), which transforms import and from … import statements into scoped entries:
PyLSPContext ctx;
py_lsp_init(&ctx, arena, source, source_len, registry, "myproj.utils", NULL);
py_lsp_add_import(&ctx, "os", "os");
py_lsp_add_import(&ctx, "json", "json");
py_lsp_bind_imports(&ctx); // Resolves imports into the current scope
Attribute resolution uses py_lookup_attribute (lines 445-452) to consult the type registry, handling inheritance and method dispatch. For expression typing, py_eval_expr_type (lines 445-558) evaluates literals, containers, and call expressions.
Self substitution and generic handling are managed by py_substitute_self (lines 780-792). Finally, resolved calls are emitted via py_emit_resolved_call_reason (lines 774-800), which creates CALLS edges annotated with confidence scores and resolution strategies (e.g., "py_lsp_method").
TypeScript and JavaScript Resolution
The TypeScript resolver (internal/cbm/lsp/ts_lsp.c) manages ES module imports, JSX component dispatch, and JSDoc-inferred signatures. It mirrors the Python architecture but adapts to JavaScript’s prototype-based inheritance and TypeScript’s structural typing rules.
Go Generics and Interfaces
In internal/cbm/lsp/go_lsp.c (~2,750 lines), the resolver maintains a per-package import registry and handles Go’s generics and interface satisfaction checking. It determines whether a concrete type implements an interface by verifying method set compatibility during the type-checking phase.
Rust Path Resolution and Generic Unification
The Rust resolver (internal/cbm/lsp/rust_lsp.c, ~3,300 lines) implements Hindley-Milner-style unification for generic substitution.
Path resolution via rust_resolve_path_expr (lines 328-379) converts qualified paths like crate::utils::Config::new into fully qualified names:
RustLSPContext rctx;
rust_lsp_init(&rctx, arena, source, len, registry, "mycrate::mod", NULL);
rust_lsp_add_use(&rctx, "Vec", "alloc::vec::Vec");
rust_lsp_bind_imports(&rctx);
const char *qn = rust_resolve_path_expr(&rctx, "crate::utils::Config::new");
// qn = "mycrate.utils.Config.new"
Type parsing uses rust_parse_type_node (lines 474-744) to convert tree-sitter type nodes into CBMType values. Generic substitution employs rust_substitute_type and rust_unify_type to instantiate concrete types:
const char *params[] = {"T", "E", NULL};
const CBMType *args[2] = {
cbm_type_named(arena, "i32"),
cbm_type_named(arena, "String")
};
const CBMType *inst = rust_substitute_type(arena,
generic_fn_type, // Result<T, E>
params, args);
From AST to Resolved Call Graph
The Hybrid LSP pipeline enriches the graph through four distinct phases:
- Import binding – Each language-specific
*_lsp_bind_importsinserts imported names into the current scope asMODULEorNAMEDtypes. - Expression typing – Evaluators recursively infer container element types, resolve generic arguments, and substitute
Self/typing.Self. - Attribute dispatch – Lookup functions (
py_lookup_attribute,rust_lookup_method_in_trait) consult the type registry built from all definitions, handling inheritance, trait implementations, and overloads. - Call emission – Once a callee qualified name and resolution strategy are determined, the resolver emits a
CBMResolvedCallthat becomes aCALLSedge in the SQLite graph stored viasrc/store/graph.c.
Performance and Deployment Benefits
The zero-runtime-dependency static binary embeds all Tree-sitter grammars and Hybrid LSP logic. Because the resolver runs in-process, indexing avoids the latency of IPC to external language servers. This architecture supports accurate cross-language call graphs—tracing a Python call into a Rust library via FFI or a TypeScript function implementing a Go interface—while maintaining sub-three-minute indexing speeds for multi-million line codebases.
Summary
- Hybrid LSP semantic type resolution combines Tree-sitter syntax with custom C resolvers to bind imports, infer generics, and resolve method dispatch across Python, TypeScript, Go, Rust, and five additional languages.
- Core implementations reside in
internal/cbm/lsp/py_lsp.c,ts_lsp.c,go_lsp.c, andrust_lsp.c, utilizing shared types frominternal/cbm/cbm.h. - The dual-pass pipeline (syntactic Tree-sitter, then semantic LSP) emits
CALLSedges with confidence scores, enabling precisetrace_pathandget_architecturequeries. - Running in-process eliminates external LSP dependencies, supporting 28 M LOC indexing in under three minutes with zero runtime dependencies.
Frequently Asked Questions
What is the difference between the Tree-sitter pass and the Hybrid LSP pass?
The Tree-sitter pass extracts raw syntactic elements—definitions, imports, and call sites—without semantic context. The Hybrid LSP pass then performs type-aware resolution, binding imports to their targets, inferring generic types, and walking inheritance chains to produce resolved call edges that reflect the actual runtime behavior of the code.
How does the Hybrid LSP handle generic types in Rust and Python?
For Rust, the resolver implements Hindley-Milner-style unification via rust_unify_type and rust_substitute_type to instantiate concrete types from generic parameters. In Python, py_substitute_self (lines 780-792) handles Self and typing.Self replacements, while py_eval_expr_type evaluates container generics to determine element types at call sites.
Can the Hybrid LSP resolve calls across programming languages?
Yes. The system maintains a unified type registry and scope model (CBMType, CBMScope) across all supported languages. This allows trace_path to follow edges from Python into Rust libraries (via FFI), TypeScript implementations of Go interfaces, or other cross-language boundaries, provided the symbols are visible in the global registry.
Why implement the LSP logic in C instead of using existing language servers?
The C implementation links directly into the static binary, eliminating process-spawning overhead and IPC latency. This enables parallel indexing of millions of lines across 9+ languages in a single process with zero runtime dependencies, whereas external LSP servers would require complex orchestration and significant memory overhead per language.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →