Appendix A: Salsa Patterns

Common patterns for working with Salsa in the Ori compiler.

Query Definition

Basic Tracked Function

#[salsa::tracked]
pub fn tokens(db: &dyn Db, file: SourceFile) -> TokenList {
    let text = file.text(db);
    lexer::tokenize(db, &text)
}

With Return Reference

#[salsa::tracked(return_ref)]
pub fn parsed(db: &dyn Db, file: SourceFile) -> ParseResult {
    let tokens = tokens(db, file);
    parser::parse(db, &tokens)
}

Accumulator Pattern

For collecting items across queries:

#[salsa::accumulator]
pub struct Diagnostics(Diagnostic);

#[salsa::tracked]
pub fn check_file(db: &dyn Db, file: SourceFile) {
    let typed = typed(db, file);
    for error in &typed.errors {
        Diagnostics::push(db, error.to_diagnostic());
    }
}

// Later: collect all accumulated diagnostics
let all_diagnostics = check_file::accumulated::<Diagnostics>(db, file);

Input Definition

#[salsa::input]
pub struct SourceFile {
    #[return_ref]
    pub text: String,

    #[return_ref]
    pub path: PathBuf,
}

// Create
let file = SourceFile::new(&db, source_text, path);

// Read
let text = file.text(&db);

// Update (triggers recomputation)
file.set_text(&mut db).to(new_text);

Interned Values

For deduplication:

#[salsa::interned]
pub struct InternedType {
    #[return_ref]
    data: TypeData,
}

// Intern a type
let interned = InternedType::new(&db, type_data);

// Same data -> same ID
let interned2 = InternedType::new(&db, type_data);
assert_eq!(interned, interned2);

// Get data back
let data = interned.data(&db);

Tracked Struct

For mutable state that Salsa tracks:

#[salsa::tracked]
pub struct TypedExpr {
    pub expr: ExprId,

    #[return_ref]
    pub ty: Type,
}

Database Setup

#[salsa::db]
pub trait Db: salsa::Database {
    fn interner(&self) -> &Interner;
}

#[salsa::db]
#[derive(Default)]
pub struct Database {
    storage: salsa::Storage<Self>,
    interner: Interner,
}

#[salsa::db]
impl salsa::Database for Database {
    fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
        // Optional: log events for debugging
        if std::env::var("ORI_DEBUG").is_ok() {
            eprintln!("[Salsa] {:?}", event());
        }
    }
}

impl Db for Database {
    fn interner(&self) -> &Interner {
        &self.interner
    }
}

Parallel Queries

// These can run in parallel automatically
let tokens_a = tokens(db, file_a);
let tokens_b = tokens(db, file_b);

// Salsa handles synchronization

Cycle Detection

Salsa detects query cycles:

// This would panic with cycle error
#[salsa::tracked]
fn a(db: &dyn Db) -> i32 {
    b(db) + 1
}

#[salsa::tracked]
fn b(db: &dyn Db) -> i32 {
    a(db) + 1  // Cycle!
}

Handle cycles explicitly:

#[salsa::tracked(cycle_fn = handle_cycle)]
fn resolve_type(db: &dyn Db, name: Name) -> Type {
    // ...
}

fn handle_cycle(_db: &dyn Db, _cycle: &[String]) -> Type {
    Type::Error
}

Early Cutoff

Salsa skips downstream recomputation when output unchanged:

// File changes slightly but tokens are same
file.set_text(&mut db).to("let x = 42 ");  // Added space

// tokens() re-runs (input changed)
// But if TokenList is equal to before...
// parsed() can skip! (early cutoff)

Requirements for early cutoff:

  • Output type must implement Eq
  • Comparison must be efficient

Testing with Salsa

#[test]
fn test_incremental() {
    let mut db = Database::default();

    // Initial compilation
    let file = SourceFile::new(&db, "let x = 1".into(), "test.ori".into());
    let result1 = typed(&db, file);

    // Modify and recompile
    file.set_text(&mut db).to("let x = 2".into());
    let result2 = typed(&db, file);

    // Verify types are same
    assert_eq!(result1.expr_types, result2.expr_types);
}

Common Mistakes

1. Forgetting Eq on Output Types

// Wrong - won't compile
#[salsa::tracked]
fn query(db: &dyn Db) -> MyType { ... }

struct MyType { ... }  // Missing Eq!

// Right
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct MyType { ... }

2. Side Effects in Queries

// Wrong - side effect in query
#[salsa::tracked]
fn tokens(db: &dyn Db, file: SourceFile) -> TokenList {
    println!("Tokenizing...");  // Side effect!
    // ...
}

// Right - use event logging
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
    println!("[Salsa] {:?}", event());
}

3. Non-Deterministic Queries

// Wrong - non-deterministic
#[salsa::tracked]
fn random_value(db: &dyn Db) -> i32 {
    rand::random()  // Different each call!
}

// Right - deterministic from inputs only

4. Large Clones

// Avoid - clones entire AST
#[salsa::tracked]
fn big_query(db: &dyn Db) -> LargeAst { ... }

// Better - return reference
#[salsa::tracked(return_ref)]
fn big_query(db: &dyn Db) -> LargeAst { ... }