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?
Created
February 26, 2026 19:30
-
-
Save oconnor663/c2a7662e3d88048b691754da957121d1 to your computer and use it in GitHub Desktop.
surprising Salsa behavior: `cycle_initial` called multiple times for the same query
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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