← Back to blog
April 8, 2026·10 min read

Shipping Multi-Language Support

EngineeringRelease

PRISM started as a Python-only tool. The core problem (knowing whether the method you're editing actually runs) exists in every language with inheritance, but the original backend was built on Python's ast module. Extending it to 9 more languages in a single release required rethinking the parsing layer without blowing the 200ms performance budget.

The problem with language-specific parsers

When we started planning multi-language support, the obvious approach was to build a parser for each language: Java with javaparser, TypeScript with the TS compiler API, C++ with libclang. Each would produce a class hierarchy that could feed into PRISM's existing resolution and status classification pipeline.

This approach has a scaling problem. Each parser has its own build system, its own version dependencies, and its own quirks. The TS compiler API requires Node.js. libclang requires a C toolchain. Maintaining 10 different parser implementations would consume more engineering time than the actual analysis logic.

Tree-sitter: one parser to rule them all

Tree-sitter is a parser generator framework originally built by Max Brunsfeld at GitHub for syntax highlighting in Atom (now used in Zed, Neovim, Helix, and others). It generates fast, incremental parsers for any language from a grammar specification.

Tree-sitter parsers produce concrete syntax trees (CSTs). They preserve every token, including whitespace and comments, and they never crash on invalid input. They just mark error nodes. For PRISM, tree-sitter offered three advantages:

  • Uniform API: Every language produces a tree with the same node traversal interface. Query patterns differ, but the infrastructure is shared.
  • Speed: Tree-sitter parsers are compiled to C and operate in linear time. Parsing a 10,000-line file takes single-digit milliseconds.
  • Error recovery: Partially written code (common during editing) still produces a usable tree. The parser doesn't bail on a missing semicolon.

The extraction pattern

For each language, PRISM needs to extract three things from a source file: class definitions (name, line, base classes), method definitions (name, line, containing class), and import statements (to resolve cross-file references). We wrote a tree-sitter query for each language that captures these nodes.

Here's what the TypeScript query looks like (simplified):

scheme
;; Match class declarations
(class_declaration
  name: (type_identifier) @class.name
  (class_heritage
    (extends_clause (identifier) @class.base))?)

;; Match method definitions
(method_definition
  name: (property_identifier) @method.name)

The same pattern works for every language. The query syntax changes, but the output format is identical: a list of classes with their bases and methods. This feeds directly into PRISM's existing resolver and status classifier, which are language-agnostic.

Language-specific resolution rules

While extraction is uniform, resolution rules differ by language. Python uses C3 linearization. Java uses single inheritance for classes (so the MRO is a simple chain) but allows multiple interface implementation with default methods. C++ uses C3-like linearization for virtual inheritance. Each language has edge cases:

  • TypeScript/JavaScript: Prototype chain is single-inheritance, but mixin patterns (Object.assign, spread) create implicit multiple inheritance that tree-sitter can detect at the syntactic level.
  • Java: Default methods in interfaces create diamond-like conflicts. The resolution rule is: class methods win over interface defaults, and more specific interfaces win over less specific ones.
  • C++: Virtual inheritance changes the object layout and method resolution. PRISM tracks the virtual keyword to adjust the hierarchy.
  • Go: No classical inheritance. Method resolution works through embedded structs (composition). PRISM treats embedded structs as bases.
  • Ruby: Mixins via include/prepend create an MRO. prepend inserts before the class; include inserts after. PRISM handles both.

Performance: staying under 200ms

Adding 9 languages to the parser could have blown our performance budget. Tree-sitter helped by being fast, but the real savings came from the cache architecture. PRISM caches parsed ASTs by file modification time. Switching between languages doesn't invalidate the cache for other files. The workspace index is shared across languages but partitioned by file extension.

In benchmarks on a mixed TypeScript/Python monorepo with 50,000 lines, cursor-move-to-panel-update averaged 87ms for Python files and 94ms for TypeScript files. Well within budget.

The result

PRISM v0.3.0 shipped with support for Python, TypeScript, JavaScript, Java, Kotlin, C++, Go, C#, Ruby, and Scala. Same four-state classification. Same sub-200ms analysis. Same UI. The tree-sitter approach turned what could have been months of per-language work into a pattern that each language could plug into.

If you're building developer tools that need to parse multiple languages, consider tree-sitter. The upfront investment in learning the query language pays off fast when you realize you can add a new language in a day instead of a month.

Try PRISM

See method resolution in real time.