# Hybrid LSP Semantic Type Resolution in Python, TypeScript, Go, and Rust: A Deep Dive into codebase-memory-mCP

> Explore hybrid LSP semantic type resolution in Python, TypeScript, Go, and Rust. Discover how codebase-memory-mCP combines ASTs and C implementations for cross-file call graphs.

- Repository: [Martin Vogel/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
- Tags: deep-dive
- Published: 2026-07-06

---

**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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/pipeline/pass_lsp_cross.c) and parallelized via [`src/pipeline/pass_parallel.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/pipeline/pass_parallel.c).

1. **Tree-sitter pass** – Fast extraction of definitions, imports, and call sites.
2. **Hybrid LSP pass** – Per-language type-aware resolver that binds imports, infers generics, walks MROs (Method Resolution Order), substitutes `Self` types, 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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/internal/cbm/cbm.h), including `CBMType` and `CBMScope`.

### Python Resolution in [`py_lsp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/py_lsp.c)

The Python resolver ([`internal/cbm/lsp/py_lsp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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:

```c
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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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:

```c
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:

```c
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_imports` inserts imported names into the current scope as `MODULE` or `NAMED` types.
- **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 `CBMResolvedCall` that becomes a `CALLS` edge in the SQLite graph stored via [`src/store/graph.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/internal/cbm/lsp/py_lsp.c), [`ts_lsp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/ts_lsp.c), [`go_lsp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/go_lsp.c), and [`rust_lsp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/rust_lsp.c), utilizing shared types from [`internal/cbm/cbm.h`](https://github.com/DeusData/codebase-memory-mcp/blob/main/internal/cbm/cbm.h).
- The dual-pass pipeline (syntactic Tree-sitter, then semantic LSP) emits `CALLS` edges with confidence scores, enabling precise `trace_path` and `get_architecture` queries.
- 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.