How KCL Uses the Salsa Crate for Incremental Compilation in Its Language Server

The salsa crate provides KCL's language server with an incremental, demand-driven computation framework that caches parser outputs, ASTs, and semantic analysis results, ensuring only changed files trigger recompilation during LSP operations.

The KCL programming language requires a responsive IDE experience to handle configuration files efficiently. To achieve fast feedback loops in its Language Server Protocol (LSP) implementation, KCL integrates the salsa crate—an incremental computation library written in Rust. This integration allows the kcl-lang/kcl repository to track fine-grained dependencies between compiler stages and avoid redundant work when users edit source files.

What the Salsa Crate Provides

The salsa crate is a framework for incremental computation that structures programs as a set of pure functions called queries. For KCL's LSP, this architecture delivers three critical capabilities that keep the editor responsive.

Query-Based Database

Every piece of compiler state resides in a salsa::Database. In crates/tools/src/LSP/src/lib.rs, KCL defines a KclDatabase struct that encapsulates all compilation state:

#[salsa::database(
    lexer::Database,
    parser::Database,
    sema::Database,
    // …
)]
pub struct KclDatabase {
    storage: salsa::Storage<Self>,
}
impl salsa::Database for KclDatabase {}

Each compiler phase—lexing, parsing, and semantic analysis—registers its queries within this database, creating a unified storage layer for incremental compilation.

Dependency Tracking and Selective Recomputation

When a query executes, salsa records which other queries it reads, constructing a fine-grained dependency graph. If a source file changes, only queries that directly or transitively depend on that file are invalidated. All other queries return cached results instantly without recomputation. This selective invalidation ensures that editing one KCL module does not force type-checking of unchanged imports.

Thread-Safe Memoization

Salsa memoizes query results internally and supports parallel execution. This design aligns with the asynchronous nature of LSP servers, allowing KCL to handle multiple concurrent requests (completions, hover information, diagnostics) without blocking the main thread or duplicating work.

Incremental Pipeline Implementation

KCL maps its compiler front-end onto salsa queries to create an incremental pipeline. The implementation splits compilation into discrete, tracked steps that mirror the traditional compiler phases.

Parser and AST Queries

The LSP defines a query to transform file contents into an abstract syntax tree. In crates/tools/src/LSP/src/lib.rs, the ast query demonstrates this pattern:

#[salsa::tracked]
pub fn ast(&self, file_id: FileId) -> Arc<Ast> {
    let source = self.source(file_id);
    parser::parse(&source)
}

This query depends on the source input (the raw file text). When a user edits a file, only that specific file's ast query is invalidated; all other files retain their cached ASTs.

Semantic Analysis Queries

Type checking and symbol resolution are implemented as separate queries that depend on the AST. The type_check function in the same file illustrates this dependency chain:

#[salsa::tracked]
pub fn type_check(&self, file_id: FileId) -> Result<TypeInfo, TypeError> {
    let ast = self.ast(file_id);
    sema::check(ast)
}

Because type_check calls self.ast(file_id), salsa automatically establishes a dependency link. If the AST is unchanged, the type-checking results are served from cache even when other queries run.

Real-Time LSP Operations

The LSP handles two primary operations: updating state when documents change and responding to IDE feature requests. Both leverage the incremental database to minimize latency.

Handling Document Changes

When the LSP receives a textDocument/didChange notification, it updates the source input in the database. The on_change handler in crates/tools/src/LSP/src/dispatcher.rs triggers invalidation:

fn on_change(&self, uri: Url, new_text: String) {
    let file_id = self.db.file_id(uri);
    self.db.set_source(file_id, new_text); // Invalidates dependent queries
}

Calling set_source marks the source query as changed, causing salsa to invalidate the ast and type_check queries for that file while leaving other modules untouched.

Serving LSP Requests

IDE features like go-to-definition read from the cached semantic state. The handle_goto_def function demonstrates how LSP requests consume the incremental database:

fn handle_goto_def(&self, params: GotoDefParams) -> Option<Location> {
    let file_id = self.db.file_id(params.text_document.uri);
    let def = self.db.find_definition(file_id, params.position)?;
    Some(def.location())
}

Here, find_definition is a salsa query that reads from the already-computed symbol tables. Because the underlying data is cached, navigation requests return instantly without recomputing the entire project state.

Dependency Configuration

The salsa integration is configured in crates/tools/src/LSP/Cargo.toml with specific feature flags to minimize binary size and compilation time:

[dependencies]
salsa = { version = "0.16.1", default-features = false }

Setting default-features = false prevents pulling in optional salsa features that KCL does not require, keeping the LSP crate lightweight.

Summary

  • The salsa crate provides KCL with a query-based, incremental computation framework that caches compiler state between edits.
  • Dependency tracking ensures that changing one file only invalidates queries for that file and its dependents, not the entire codebase.
  • Thread-safe memoization allows the LSP to handle concurrent requests efficiently while maintaining consistent state.
  • Source files and request handlers in crates/tools/src/LSP/src/dispatcher.rs and lib.rs demonstrate how document changes trigger minimal recomputation and how IDE features consume cached results.

Frequently Asked Questions

What is incremental compilation in an LSP context?

Incremental compilation in a Language Server means the compiler reuses previous analysis results when source code changes. Instead of parsing and type-checking the entire project on every keystroke, the system identifies exactly which functions, modules, or expressions changed and recomputes only the affected portions. This keeps editor feedback latency low even in large codebases.

How does the salsa crate differ from traditional build systems?

Traditional build systems like Make or Cargo typically track file-level dependencies and rebuild entire compilation units when inputs change. The salsa crate operates at a finer granularity, tracking function-level dependencies (queries) and their specific arguments. It provides an in-memory, incremental computation engine rather than a disk-based artifact cache, making it ideal for long-running language servers that must respond to micro-edits in milliseconds.

Why did KCL choose salsa for its language server over other approaches?

KCL selected salsa because it provides a declarative, Rust-native API for demand-driven computation that fits naturally with the compiler's query-based architecture. The crate handles the complex logic of cache invalidation, parallel execution, and consistency automatically, allowing the KCL team to focus on language semantics rather than implementing incremental algorithms manually. As shown in crates/tools/src/LSP/src/lib.rs, the #[salsa::tracked] attribute macro requires minimal boilerplate to add incremental capabilities to compiler functions.

How does KCL ensure type consistency across modules during incremental updates?

KCL ensures consistency through salsa's database revisions. When set_source is called in crates/tools/src/LSP/src/dispatcher.rs, salsa atomically advances the database revision, invalidating all queries that transitively depend on the changed source. Subsequent LSP requests observe a consistent snapshot of the program state where all cached results are valid for the current revision, while stale results are transparently recomputed. This guarantees that go-to-definition and hover information always reflect the current code structure without manual cache management.

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 →