Skip to main content
MCC uses Salsa to enable incremental compilation. When you recompile after making changes, Salsa automatically reuses cached results from unchanged stages, significantly speeding up development cycles.

What is Salsa?

Salsa is a framework for incremental computation in Rust. It tracks dependencies between function calls and automatically recomputes only what’s necessary when inputs change.

Tracked functions

Functions annotated with #[salsa::tracked] are automatically memoized

Input tracking

Salsa tracks which inputs each function reads

Smart recomputation

Only reruns functions when their inputs change

Accumulators

Collects diagnostics without breaking pure functions

The Database trait

All compilation functions take a &dyn Db parameter. This trait represents the Salsa database:
// crates/mcc/src/lib.rs:111
#[salsa::db]
pub trait Db: salsa::Database {}

#[salsa::db]
impl<T: salsa::Database> Db for T {}
The concrete implementation stores Salsa’s internal state:
// crates/mcc/src/lib.rs:118
#[salsa::db]
#[derive(Default, Clone)]
pub struct Database {
    storage: salsa::Storage<Self>,
}

#[salsa::db]
impl salsa::Database for Database {}
The Database struct is cheap to clone. Cloning creates a new view with the same cached results.

Tracked functions

Each compilation stage is a tracked function. Salsa automatically memoizes the results:
// crates/mcc/src/parsing.rs:15
#[salsa::tracked]
pub fn parse(db: &dyn Db, file: SourceFile) -> Ast<'_> {
    let mut parser = tree_sitter::Parser::new();
    parser.set_language(&tree_sitter::Language::new(tree_sitter_c::LANGUAGE))
        .unwrap();
    
    let src = file.contents(db);
    let tree = Tree::from(parser.parse(src, None).unwrap());
    
    check_tree(db, &tree, file);
    Ast::new(db, tree)
}
When you call parse(&db, file) again with the same file, Salsa returns the cached Ast without re-parsing.

Tracked structs

Salsa can also track structs, making them efficient to store and compare:
// crates/mcc/src/typechecking/hir.rs:14
#[salsa::tracked(debug)]
pub struct TranslationUnit<'db> {
    #[returns(ref)]
    pub items: Vec<Item<'db>>,
    pub file: SourceFile,
}

#[salsa::tracked(debug)]
pub struct FunctionDefinition<'db> {
    pub name: Identifier<'db>,
    pub node: ptr::FunctionDefinition<'db>,
}
Tracked structs are lightweight handles. The actual data is stored in the Salsa database:
// Allocates in the database
let tu = hir::TranslationUnit::new(db, items, file);

Interned values

Identifiers are interned for efficient comparison:
// crates/mcc/src/typechecking/hir.rs:42
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct Identifier {
    pub text: Text,
}

impl<'db> Identifier<'db> {
    pub fn from_node(
        db: &'db dyn Db,
        file: SourceFile,
        node: mcc_syntax::ast::Identifier<'db>,
    ) -> Self {
        let src = file.contents(db);
        let name = node.utf8_text(src.as_bytes()).expect("unreachable");
        Identifier::new(db, Text::from(name))
    }
}
Interning ensures that identical identifiers share the same storage and can be compared with simple pointer equality.

Accumulators for diagnostics

Salsa accumulators allow tracked functions to “push” diagnostics without returning them:
// crates/mcc/src/diagnostics.rs
#[salsa::accumulator]
pub struct Diagnostics(Diagnostic);

impl DiagnosticExt for Diagnostic {
    fn accumulate(self, db: &dyn crate::Db) {
        Diagnostics::push(db, self);
    }
}
Stages accumulate diagnostics during compilation:
// crates/mcc/src/typechecking/mod.rs:82
Diagnostic::error()
    .with_message("Expected a return type for function")
    .with_code(codes::type_check::missing_return_type)
    .with_labels(vec![
        Label::primary(file, Span::for_node(*raw))
            .with_message("error occurred here"),
    ])
    .accumulate(db);  // Pushed to accumulator
Retrieve accumulated diagnostics after a stage completes:
// crates/mcc/src/lowering/mod.rs:700
pub fn lower_stage_diagnostics(
    db: &dyn Db,
    file: SourceFile,
) -> Vec<&crate::diagnostics::Diagnostics> {
    let typecheck_diags = 
        crate::typechecking::typecheck::accumulated::<Diagnostics>(db, file);
    let lower_diags = 
        lower_program::accumulated::<Diagnostics>(db, file);
    typecheck_diags.into_iter().chain(lower_diags).collect()
}
Accumulators solve a key problem: how to collect diagnostics while keeping compilation functions pure (returning a single value).Without accumulators, you’d need to return (Result, Vec<Diagnostic>) from every function, complicating signatures and making composition harder.With accumulators:
  • Functions remain simple: fn parse(db: &dyn Db, file: SourceFile) -> Ast
  • Diagnostics are collected separately: parse::accumulated::<Diagnostics>(db, file)
  • Salsa tracks accumulated values just like function results

How dependency tracking works

Salsa automatically tracks dependencies between function calls:
1

First compilation

let db = Database::default();
let file = SourceFile::new(&db, "main.c".into(), "int main() {}".into());
let tacky = lower_program(&db, file);
Salsa records that:
  • lower_program(file) called typecheck(file)
  • typecheck(file) called parse(file)
  • parse(file) read file.contents()
2

Source file changes

// Modify the source
let file = SourceFile::new(&db, "main.c".into(), "int main() { return 1; }".into());
Salsa marks file.contents() as changed.
3

Recompilation

let tacky = lower_program(&db, file);
Salsa automatically:
  1. Detects that file.contents() changed
  2. Invalidates parse(file) (depends on contents)
  3. Invalidates typecheck(file) (depends on parse)
  4. Invalidates lower_program(file) (depends on typecheck)
  5. Reruns all invalidated functions
4

Unchanged dependencies are reused

If a function depends on something that didn’t change, it returns the cached result immediately.For example, if you only change code generation logic (not source code), parsing, typechecking, and lowering all return cached results.

Incremental compilation example

let db = Database::default();
let file = SourceFile::new(
    &db, 
    "main.c".into(), 
    "int main(void) { return 0; }".into()
);

// First compilation - nothing cached
let ast = parse(&db, file);              // Runs parser
let hir = typecheck(&db, file);          // Runs typechecker
let tacky = lower_program(&db, file);    // Runs lowering
let asm = generate_assembly(&db, tacky); // Runs codegen

Per-function caching

Salsa tracks individual functions within a program:
// crates/mcc/src/codegen/mod.rs:22
#[salsa::tracked]
fn lower_function(db: &dyn Db, function: tacky::FunctionDefinition<'_>) 
    -> asm::FunctionDefinition<'_> {
    let asm = to_assembly(db, function);
    fix_up_instructions(db, asm)
}
If a program has multiple functions and you only modify one, Salsa only recompiles that function.
Currently MCC only supports single-function programs (main), but the infrastructure supports multi-function caching.

Benefits of incremental compilation

Fast iteration cycles - Recompiling after small changes is nearly instant
Efficient testing - Test suites reuse cached results across test cases
Lazy evaluation - Only computes what’s actually needed
Composability - Stages can call each other freely without worrying about redundant work

Example usage

// Quick start from lib.rs:27
use mcc::{Database, SourceFile, Text};

let db = Database::default();
let src = "int main(void) { return 0; }";
let file = SourceFile::new(&db, Text::from("main.c"), Text::from(src));

// Parse → typecheck → TAC → ASM IR → assembly text
let ast = mcc::parse(&db, file);
let tacky = mcc::lowering::lower_program(&db, file);
let asm_ir = mcc::codegen::generate_assembly(&db, tacky);
let asm_text = mcc::render_program(&db, asm_ir, mcc::default_target());

assert!(asm_text.as_str().contains("main"));

// Compile again - all stages return cached results
let asm_text = mcc::render_program(
    &db, 
    mcc::codegen::generate_assembly(
        &db, 
        mcc::lowering::lower_program(&db, file)
    ),
    mcc::default_target()
);

Learn more

Salsa documentation

Official Salsa documentation and examples

Architecture

Learn about MCC’s overall architecture

Pipeline

Understand the seven compilation stages

Salsa book

Deep dive into Salsa’s design and implementation

Build docs developers (and LLMs) love