How the Hybrid LSP Layer Resolves Types Across Python, TypeScript, and Go

The Hybrid LSP layer in Codebase-Memory-MCP resolves types across multiple programming languages by normalizing definitions into a language-agnostic format, building a global module index, and dispatching to optimized language-specific resolvers that filter symbols based on import graphs and qualified names.

The Codebase-Memory-MCP (CBM) project implements a sophisticated cross-language type resolution system through its Hybrid Language Server Protocol (LSP) layer. Unlike traditional LSP implementations that operate on single files, this hybrid architecture enables the resolution of type information across file boundaries and even between different programming languages. The core mechanism resides in the cross-file LSP pass (cbm_pipeline_pass_lsp_cross) located in src/pipeline/pass_lsp_cross.c, which orchestrates a unified pipeline supporting Go, Python, TypeScript, Java, Kotlin, Rust, C/C++, C#, and PHP.

The Eight-Stage Cross-File Resolution Pipeline

The Hybrid LSP layer processes every file through a standardized eight-stage pipeline that maintains language neutrality while leveraging optimized language-specific backends.

Step 1: Collecting Project-Wide Definitions

The pass begins by gathering all definitions from every parsed file in the project. The function cbm_pxc_collect_all_defs converts each language-specific CBMDefinition into a generic CBMLSPDef structure that captures qualified names, short names, labels, receiver types, return types, and embedded types.

CBMLSPDef *defs = cbm_pxc_collect_all_defs(cache, files, file_count,
                                          ctx->project_name, def_modules, &def_count);

This normalization allows the subsequent stages to treat Python classes, Go interfaces, and TypeScript types uniformly.

Step 2: Building the Module-Definition Index

To enable fast lookups, the system constructs a CBMModuleDefIndex hash table that maps module qualified names (QNs) to their constituent definitions. For example, github.com/example/pkg or Java package namespaces point directly to the indices of relevant CBMLSPDef entries.

CBMModuleDefIndex *mod_idx = cbm_pxc_build_module_def_index(defs, def_count);

This index allows the resolver to filter thousands of project-wide definitions down to only those relevant to a specific file in constant time.

Step 3: Creating Language-Specific Shared Registries

For languages that support pre-built cross-registries (Go, Python, C, C#, and TypeScript), the pass creates a shared CBMTypeRegistry once per pipeline run. These compact, lookup-optimized structures store the full definition set in memory-efficient formats specific to each language's resolution patterns.

// Example for Go (similar functions exist for other languages)
cbm_go_build_cross_registry(defs, def_count, &cross_registries);

Step 4: Mapping Imports for Each File

Using the graph buffer (gbuf), the function pxc_build_import_map extracts IMPORTS edges to build a map of local import names to resolved module QNs. This tells the resolver exactly which external modules a file can legally reference.

pxc_build_import_map(gbuf, ctx->project_name, file->rel_path,
                    &import_keys, &import_vals, &import_count);

Step 5: Dispatching to Language-Specific Resolvers

The cbm_pxc_dispatch_file function serves as the central router. It first filters the global definition array to include only the current module and its imports using cbm_pxc_filter_defs_for_file. Then it chooses the resolution strategy:

  • Shared registry path: For languages with cross-registries, it calls fast overlay resolvers like cbm_go_fast_resolve_qualified_calls
  • Per-file LSP path: Falls back to cbm_pxc_run_one or cbm_pxc_run_one_ts for languages without shared registries
cbm_pxc_dispatch_file(lang, cache[i], source, src_len, file->rel_path,
                      def_modules[i], &cross_registries, mod_idx,
                      defs, def_count, import_keys, import_vals, import_count,
                      NULL, NULL);

Step 6: Language-Specific Cross-File Resolution

Each language implements a dedicated resolver that receives the filtered definitions, import map, source buffer, and AST. These functions walk the file's syntax tree and match symbols against the supplied CBMLSPDef array:

void cbm_run_go_lsp_cross(CBMArena *arena,
                          const char *src, int src_len,
                          const char *module_qn,
                          CBMLSPDef *defs, int def_cnt,
                          const char **imp_names, const char **imp_qns,
                          int imp_cnt, TSTree *tree,
                          CBMResolvedCallArray *out) {
    // Parse AST and lookup symbols using the filtered defs and import map
}

Step 7: Merging and Deduplicating Results

After resolution, pxc_append_results merges the language-specific outputs into the file's resolved_calls arena. A temporary hash-set eliminates duplicate (caller → callee) pairs that could arise from multiple import paths or re-exports.

pxc_append_results(&result->arena, &result->resolved_calls, &out);

Step 8: Resource Cleanup

Once all files are processed, the pass frees the module-definition index and global definition array. Shared registries remain cached for the pipeline duration, while Rust-specific resources like parsed Cargo.toml manifests (handled via cbm_cargo_parse) are released if present.

Cross-Language Type Resolution Mechanics

The "hybrid" aspect emerges from the system's ability to resolve types across language boundaries. When a Go file imports a Python package (such as via cgo-generated bindings), the Go resolver can still locate Python symbols because the global definition list already contains Python CBMLSPDef entries. The import map identifies which module QNs are visible, and the generic filtering logic works regardless of the source language.

Special Handling for JVM Languages

Java and Kotlin derive their module qualified names from the containing directory rather than file stems. Helper functions like pxc_module_is_dir and pxc_infer_jvm_namespace ensure that the module QN matches the Java/Kotlin package hierarchy, enabling accurate cross-resolution between JVM languages and other systems.

Rust Workspace Awareness

When a Cargo.toml manifest is detected, the pass parses it using cbm_cargo_parse and stores a CBMCargoManifest. The Rust resolver (cbm_run_rust_lsp_cross_with_manifest) then uses this manifest to resolve crate-to-crate references across workspace boundaries.

Key Implementation Files

The Hybrid LSP layer spans multiple source files:

Summary

  • The Hybrid LSP layer normalizes all language definitions into a generic CBMLSPDef format, enabling a unified resolution pipeline.

  • A module-definition index (CBMModuleDefIndex) provides O(1) lookups of symbols by module qualified name.

  • Language-specific shared registries optimize performance for Go, Python, TypeScript, C, and C# by pre-building compact lookup structures.

  • The import map and filtering logic ensure resolvers only see definitions reachable from the current file, even across language boundaries.

  • Dedicated resolver functions in internal/cbm/lsp/ handle language-specific AST walking while consuming the language-agnostic definition array.

  • Specialized handling for JVM directory-based modules and Rust Cargo workspaces ensures accurate cross-language resolution in complex project structures.

Frequently Asked Questions

How does the Hybrid LSP layer handle cross-language imports?

The Hybrid LSP layer handles cross-language imports by maintaining a global CBMLSPDef array that contains normalized definitions from all languages. When resolving a file, the system builds an import map that associates local import aliases with module qualified names (QNs). The dispatcher then filters the global definition list to include only definitions from the current module and its imported modules, regardless of language. This allows a Go resolver to find Python symbols when the import map indicates a cross-language dependency exists.

What is the difference between the shared registry and per-file LSP resolution?

The shared registry path creates a compact, pre-built CBMTypeRegistry once per pipeline run for languages like Go, Python, and TypeScript. This registry enables fast "overlay" resolution via functions like cbm_go_fast_resolve_qualified_calls. The per-file LSP path (cbm_pxc_run_one) processes definitions on a per-file basis without pre-built structures, serving as a fallback for languages without shared registry implementations or when memory constraints prevent caching. The dispatcher automatically selects the fastest available path based on the target language.

How are JVM languages like Java and Kotlin handled differently?

JVM languages use directory-based module qualified names rather than file-based names. The pxc_module_is_dir helper and pxc_infer_jvm_namespace functions extract the package hierarchy from the directory structure to construct accurate module QNs that match Java and Kotlin package declarations. This ensures that when Java files import Kotlin classes (or vice versa), the module index correctly associates the definitions with their namespace paths, enabling accurate cross-resolution between JVM languages.

Can the Hybrid LSP layer resolve types in Rust workspaces?

Yes, the Hybrid LSP layer includes specific support for Rust workspaces through Cargo manifest parsing. When a Cargo.toml file is present, the pass invokes cbm_cargo_parse to create a CBMCargoManifest. The Rust resolver (cbm_run_rust_lsp_cross_with_manifest) uses this manifest to understand crate boundaries and dependencies, allowing it to resolve types across multiple crates within a workspace. The manifest is stored during the pipeline run and freed during the cleanup phase after all files are processed.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →