Skip to content

Instantly share code, notes, and snippets.

@oconnor663
Created February 26, 2026 19:30
Show Gist options
  • Select an option

  • Save oconnor663/c2a7662e3d88048b691754da957121d1 to your computer and use it in GitHub Desktop.

Select an option

Save oconnor663/c2a7662e3d88048b691754da957121d1 to your computer and use it in GitHub Desktop.
surprising Salsa behavior: `cycle_initial` called multiple times for the same query

It seems like the cycle_initial of a query can be called more than once, if the query alternates between being a cycle head and not being one. To start with, here's example of a mutually recursive Salsa program that eventually stabilizes, by fixpoint-iterating an integer until it reaches some upper bound (3). Then there's a modified version of that program that makes the inner query sometimes call itself directly. This version never stabilizes, because the directly recursive call always returns cycle_initial (0). I find that surprising. Is this an intended behavior?

use salsa::Database;
#[salsa::db]
#[derive(Default, Clone)]
struct Db {
storage: salsa::Storage<Self>,
}
#[salsa::db]
impl Database for Db {}
#[salsa::tracked(cycle_initial = outer_initial)]
fn outer(db: &dyn Database) -> u32 {
println!("\nouter calls inner...");
let i = inner(db);
if i >= 3 {
println!("inner ({i}) is big enough, stabilize here");
return i;
}
println!("outer returns inner+1 ({})", i + 1);
i + 1
}
fn outer_initial(_: &dyn Database, _: salsa::Id) -> u32 {
println!("outer_initial (0)");
0
}
#[salsa::tracked(cycle_initial = inner_initial)]
fn inner(db: &dyn Database) -> u32 {
println!("inner calls outer...");
let o = outer(db);
println!("inner returns outer ({o})");
o
}
fn inner_initial(_: &dyn Database, _: salsa::Id) -> u32 {
println!("inner_initial (0)");
0
}
fn main() {
let db = Db::default();
outer(&db);
}
outer calls inner...
inner calls outer...
outer_initial (0)
inner returns outer (0)
outer returns inner+1 (1)
outer calls inner...
inner calls outer...
inner returns outer (1)
outer returns inner+1 (2)
outer calls inner...
inner calls outer...
inner returns outer (2)
outer returns inner+1 (3)
outer calls inner...
inner calls outer...
inner returns outer (3)
inner (3) is big enough, stabilize here
diff --git a/src/main.rs b/src/main.rs
index 0d94a25..58eff2d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,8 +30,14 @@ fn outer_initial(_: &dyn Database, _: salsa::Id) -> u32 {
fn inner(db: &dyn Database) -> u32 {
println!("inner calls outer...");
let o = outer(db);
- println!("inner returns outer ({o})");
- o
+ if o < 3 {
+ println!("inner returns outer ({o})");
+ o
+ } else {
+ let i = inner(db);
+ println!("inner returns itself ({i})");
+ i
+ }
}
fn inner_initial(_: &dyn Database, _: salsa::Id) -> u32 {
use salsa::Database;
#[salsa::db]
#[derive(Default, Clone)]
struct Db {
storage: salsa::Storage<Self>,
}
#[salsa::db]
impl Database for Db {}
#[salsa::tracked(cycle_initial = outer_initial)]
fn outer(db: &dyn Database) -> u32 {
println!("\nouter calls inner...");
let i = inner(db);
if i >= 3 {
println!("inner ({i}) is big enough, stabilize here");
return i;
}
println!("outer returns inner+1 ({})", i + 1);
i + 1
}
fn outer_initial(_: &dyn Database, _: salsa::Id) -> u32 {
println!("outer_initial (0)");
0
}
#[salsa::tracked(cycle_initial = inner_initial)]
fn inner(db: &dyn Database) -> u32 {
println!("inner calls outer...");
let o = outer(db);
if o < 3 {
println!("inner returns outer ({o})");
o
} else {
let i = inner(db);
println!("inner returns itself ({i})");
i
}
}
fn inner_initial(_: &dyn Database, _: salsa::Id) -> u32 {
println!("inner_initial (0)");
0
}
fn main() {
let db = Db::default();
outer(&db);
}
outer calls inner...
inner calls outer...
outer_initial (0)
inner returns outer (0)
outer returns inner+1 (1)
outer calls inner...
inner calls outer...
inner returns outer (1)
outer returns inner+1 (2)
outer calls inner...
inner calls outer...
inner returns outer (2)
outer returns inner+1 (3)
outer calls inner...
inner calls outer...
inner_initial (0)
inner returns itself (0)
outer returns inner+1 (1)
outer calls inner...
inner calls outer...
inner returns outer (1)
outer returns inner+1 (2)
outer calls inner...
inner calls outer...
inner returns outer (2)
outer returns inner+1 (3)
outer calls inner...
inner calls outer...
inner_initial (0)
inner returns itself (0)
outer returns inner+1 (1)
[1000 lines omitted...]
thread 'main' (190835) panicked at /home/jacko/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/salsa-0.26.0/src/function/execute.rs:633:9:
inner(Id(400)): execute: too many cycle iterations
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment