Repository: rust-lang/rust-analyzer (2026-04-16T06:27:41Z)

Issue:
Crash "Can't merge cycle heads variances_of_query(Id(a0b7)) with different iteration counts" on cyclic types
Given the following minimised Rust code (derived from [this file](https://github.com/facebook/flow/blob/95cc85583a1af0f3bdf0f9467bd4de9c15cc0cef/rust_port/crates/flow_parser/src/ast.rs)):

```rust
enum Type<T> {
    Object(Box<Expression<T>>),
    Generic(Box<Type<T>>),
}

enum DeclareClassExtends<T> {
    Ident(Type<T>),
    Recursive(Box<DeclareClassExtends<T>>),
}

enum Statement<T> {
    Class(Class<T>),
    Declare(DeclareClassExtends<T>),
}

enum Expression<T> {
    Call(Type<T>),
    Class(Class<T>),
}

struct Class<T>(Box<Statement<T>>, Box<Expression<T>>);

fn make() -> Statement<()> {
    loop {}
}
```

rust-analyzer crashes, both in normal LSP usage and in analysis-stats:

```
$ rust-analyzer analysis-stats .
...
8/9 88% processing: crashdemo::make
thread 'main' (43573436) panicked at /Users/wilfred/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/salsa-0.25.2/src/cycle.rs:338:17:
assertion `left == right` failed: Can't merge cycle heads variances_of_query(Id(a0b7)) with different iteration counts (IterationCount(0), IterationCount(1))
  left: IterationCount(0)
 right: IterationCount(1)
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
   4: salsa::cycle::CycleHeads::insert
   5: salsa::active_query::ActiveQuery::add_read
   6: std::thread::local::LocalKey<T>::with
   7: hir_ty::variance::variances_of
   8: hir_ty::variance::Context::add_constraints_from_ty
   9: <hir_ty::variance::variances_of_query::variances_of_query_Configuration_ as salsa::function::Configuration>::execute
  10: salsa::function::execute::<impl salsa::function::IngredientImpl<C>>::execute
  11: salsa::function::fetch::<impl salsa::function::IngredientImpl<C>>::fetch_cold
  12: std::thread::local::LocalKey<T>::with
  13: hir_ty::variance::variances_of
  14: <hir_ty::next_solver::interner::DbInterner as ra_ap_rustc_type_ir::interner::Interner>::variances_of
  15: <hir_ty::next_solver::infer::relate::generalize::Generalizer as ra_ap_rustc_type_ir::relate::TypeRelation<hir_ty::next_solver::interner::DbInterner>>::relate_ty_args
  16: ra_ap_rustc_type_ir::relate::structurally_relate_tys
  17: <hir_ty::next_solver::infer::relate::generalize::Generalizer as ra_ap_rustc_type_ir::relate::TypeRelation<hir_ty::next_solver::interner::DbInterner>>::tys
  18: hir_ty::next_solver::infer::context::<impl ra_ap_rustc_type_ir::infer_ctxt::InferCtxtLike for hir_ty::next_solver::infer::InferCtxt>::instantiate_ty_var_raw
  19: <ra_ap_rustc_type_ir::relate::solver_relating::SolverRelating<Infcx,I> as ra_ap_rustc_type_ir::relate::TypeRelation<I>>::tys
  20: <Infcx as ra_ap_rustc_type_ir::relate::solver_relating::RelateExt>::relate
  21: hir_ty::infer::coerce::Coerce<D>::unify_raw
  22: hir_ty::infer::coerce::Coerce<D>::unify_and
  23: hir_ty::infer::coerce::Coerce<D>::coerce
  24: hir_ty::infer::coerce::<impl hir_ty::infer::InferenceContext>::coerce
  25: hir_ty::infer::coerce::CoerceMany::coerce_inner
  26: hir_ty::infer::expr::<impl hir_ty::infer::InferenceContext>::infer_return
  27: hir_ty::infer::infer_query_with_inspect
  28: salsa::function::execute::<impl salsa::function::IngredientImpl<C>>::execute
  29: salsa::function::fetch::<impl salsa::function::IngredientImpl<C>>::fetch_cold
  30: std::thread::local::LocalKey<T>::with
  31: hir_ty::infer::InferenceResult::for_body
  32: rust_analyzer::cli::analysis_stats::<impl rust_analyzer::cli::flags::AnalysisStats>::run_inference
  33: std::thread::local::LocalKey<T>::with
  34: rust_analyzer::cli::analysis_stats::<impl rust_analyzer::cli::flags::AnalysisStats>::run
  35: rust_analyzer::main
```

This looks like an issue with salsa cycles?

I've noticed this crash occurring quite a bit recently: not sure if the folks I support have been unlucky with their codebases, or if it's a more common occurrence.


Discussion:
confirmed that https://github.com/rust-lang/rust-analyzer/pull/22061 fixes this



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 88f5c33cb272..5534fb81a828 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2453,9 +2453,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 
 [[package]]
 name = "salsa"
-version = "0.25.2"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e2aa2fca57727371eeafc975acc8e6f4c52f8166a78035543f6ee1c74c2dcc"
+checksum = "f77debccd43ba198e9cee23efd7f10330ff445e46a98a2b107fed9094a1ee676"
 dependencies = [
  "boxcar",
  "crossbeam-queue",
@@ -2478,15 +2478,15 @@ dependencies = [
 
 [[package]]
 name = "salsa-macro-rules"
-version = "0.25.2"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfc2a1e7bf06964105515451d728f2422dedc3a112383324a00b191a5c397a3"
+checksum = "ea07adbf42d91cc076b7daf3b38bc8168c19eb362c665964118a89bc55ef19a5"
 
 [[package]]
 name = "salsa-macros"
-version = "0.25.2"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d844c1aa34946da46af683b5c27ec1088a3d9d84a2b837a108223fd830220e1"
+checksum = "d16d4d8b66451b9c75ddf740b7fc8399bc7b8ba33e854a5d7526d18708f67b05"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 0ae65922d007..0d00297e570c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -135,13 +135,13 @@ rayon = "1.10.0"
 rowan = "=0.15.18"
 # Ideally we'd not enable the macros feature but unfortunately the `tracked` attribute does not work
 # on impls without it
-salsa = { version = "0.25.2", default-features = false, features = [
+salsa = { version = "0.26", default-features = false, features = [
     "rayon",
     "salsa_unstable",
     "macros",
     "inventory",
 ] }
-salsa-macros = "0.25.2"
+salsa-macros = "0.26"
 semver = "1.0.26"
 serde = { version = "1.0.219" }
 serde_derive = { version = "1.0.219" }
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 8a6ef6418063..9f8374f62597 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -64,6 +64,7 @@ use cfg::CfgOptions;
 use fetch_crates::CrateInfo;
 use hir::{ChangeWithProcMacros, EditionedFileId, crate_def_map, sym};
 use ide_db::base_db::relevant_crates;
+use ide_db::base_db::salsa::Durability;
 use ide_db::line_index;
 use ide_db::ra_fixture::RaFixtureAnalysis;
 use ide_db::{
@@ -203,10 +204,18 @@ impl AnalysisHost {
         self.db.per_query_memory_usage()
     }
     pub fn trigger_cancellation(&mut self) {
-        self.db.trigger_cancellation();
+        // We need to do a synthetic write right now due to how fixpoint cycles handle cancellation
+        // the revision bump there is a reset marker for clearing fixpoint poisoning.
+        // That is `trigger_cancellation` is currently bugged wrt to cancellation.
+        // self.db.trigger_cancellation();
+        self.db.synthetic_write(Durability::LOW);
     }
     pub fn trigger_garbage_collection(&mut self) {
-        self.db.trigger_lru_eviction();
+        // We need to do a synthetic write right now due to how fixpoint cycles handle cancellation
+        // the revision bump there is a reset marker for clearing fixpoint poisoning.
+        // That is `trigger_lru_eviction` is currently bugged wrt to cancellation.
+        // self.db.trigger_lru_eviction();
+        self.db.synthetic_write(Durability::LOW);
         // SAFETY: `trigger_lru_eviction` triggers cancellation, so all running queries were canceled.
         unsafe { hir::collect_ty_garbage() };
     }
diff --git a/crates/span/src/hygiene.rs b/crates/span/src/hygiene.rs
index fe05ef946518..0a81cef52ec5 100644
--- a/crates/span/src/hygiene.rs
+++ b/crates/span/src/hygiene.rs
@@ -81,25 +81,24 @@ const _: () = {
     #[derive(Hash)]
     struct StructKey<'db, T0, T1, T2, T3>(T0, T1, T2, T3, std::marker::PhantomData<&'db ()>);
 
-    impl<'db, T0, T1, T2, T3> zalsa_::interned::HashEqLike<StructKey<'db, T0, T1, T2, T3>>
-        for SyntaxContextData
+    impl<'db, T0, T1, T2, T3> zalsa_::HashEqLike<StructKey<'db, T0, T1, T2, T3>> for SyntaxContextData
     where
-        Option<MacroCallId>: zalsa_::interned::HashEqLike<T0>,
-        Transparency: zalsa_::interned::HashEqLike<T1>,
-        Edition: zalsa_::interned::HashEqLike<T2>,
-        SyntaxContext: zalsa_::interned::HashEqLike<T3>,
+        Option<MacroCallId>: zalsa_::HashEqLike<T0>,
+        Transparency: zalsa_::HashEqLike<T1>,
+        Edition: zalsa_::HashEqLike<T2>,
+        SyntaxContext: zalsa_::HashEqLike<T3>,
     {
         fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
-            zalsa_::interned::HashEqLike::<T0>::hash(&self.outer_expn, &mut *h);
-            zalsa_::interned::HashEqLike::<T1>::hash(&self.outer_transparency, &mut *h);
-            zalsa_::interned::HashEqLike::<T2>::hash(&self.edition, &mut *h);
-            zalsa_::interned::HashEqLike::<T3>::hash(&self.parent, &mut *h);
+            zalsa_::HashEqLike::<T0>::hash(&self.outer_expn, &mut *h);
+            zalsa_::HashEqLike::<T1>::hash(&self.outer_transparency, &mut *h);
+            zalsa_::HashEqLike::<T2>::hash(&self.edition, &mut *h);
+            zalsa_::HashEqLike::<T3>::hash(&self.parent, &mut *h);
         }
         fn eq(&self, data: &StructKey<'db, T0, T1, T2, T3>) -> bool {
-            zalsa_::interned::HashEqLike::<T0>::eq(&self.outer_expn, &data.0)
-                && zalsa_::interned::HashEqLike::<T1>::eq(&self.outer_transparency, &data.1)
-                && zalsa_::interned::HashEqLike::<T2>::eq(&self.edition, &data.2)
-                && zalsa_::interned::HashEqLike::<T3>::eq(&self.parent, &data.3)
+            zalsa_::HashEqLike::<T0>::eq(&self.outer_expn, &data.0)
+                && zalsa_::HashEqLike::<T1>::eq(&self.outer_transparency, &data.1)
+                && zalsa_::HashEqLike::<T2>::eq(&self.edition, &data.2)
+                && zalsa_::HashEqLike::<T3>::eq(&self.parent, &data.3)
         }
     }
     impl zalsa_struct_::Configuration for SyntaxContext {
@@ -203,10 +202,10 @@ const _: () = {
     impl<'db> SyntaxContext {
         pub fn new<
             Db,
-            T0: zalsa_::interned::Lookup<Option<MacroCallId>> + std::hash::Hash,
-            T1: zalsa_::interned::Lookup<Transparency> + std::hash::Hash,
-            T2: zalsa_::interned::Lookup<Edition> + std::hash::Hash,
-            T3: zalsa_::interned::Lookup<SyntaxContext> + std::hash::Hash,
+            T0: zalsa_::Lookup<Option<MacroCallId>> + std::hash::Hash,
+            T1: zalsa_::Lookup<Transparency> + std::hash::Hash,
+            T2: zalsa_::Lookup<Edition> + std::hash::Hash,
+            T3: zalsa_::Lookup<SyntaxContext> + std::hash::Hash,
         >(
             db: &'db Db,
             outer_expn: T0,
@@ -218,10 +217,10 @@ const _: () = {
         ) -> Self
         where
             Db: ?Sized + salsa::Database,
-            Option<MacroCallId>: zalsa_::interned::HashEqLike<T0>,
-            Transparency: zalsa_::interned::HashEqLike<T1>,
-            Edition: zalsa_::interned::HashEqLike<T2>,
-            SyntaxContext: zalsa_::interned::HashEqLike<T3>,
+            Option<MacroCallId>: zalsa_::HashEqLike<T0>,
+            Transparency: zalsa_::HashEqLike<T1>,
+            Edition: zalsa_::HashEqLike<T2>,
+            SyntaxContext: zalsa_::HashEqLike<T3>,
         {
             let (zalsa, zalsa_local) = db.zalsas();
 
@@ -236,10 +235,10 @@ const _: () = {
                     std::marker::PhantomData,
                 ),
                 |id, data| SyntaxContextData {
-                    outer_expn: zalsa_::interned::Lookup::into_owned(data.0),
-                    outer_transparency: zalsa_::interned::Lookup::into_owned(data.1),
-                    edition: zalsa_::interned::Lookup::into_owned(data.2),
-                    parent: zalsa_::interned::Lookup::into_owned(data.3),
+                    outer_expn: zalsa_::Lookup::into_owned(data.0),
+                    outer_transparency: zalsa_::Lookup::into_owned(data.1),
+                    edition: zalsa_::Lookup::into_owned(data.2),
+                    parent: zalsa_::Lookup::into_owned(data.3),
                     opaque: opaque(zalsa_::FromId::from_id(id)),
                     opaque_and_semiopaque: opaque_and_semiopaque(zalsa_::FromId::from_id(id)),
                 },


Test patch:
diff --git a/crates/hir-ty/src/tests/regression.rs b/crates/hir-ty/src/tests/regression.rs
index a559ad9b91d6..92b1b14fa069 100644
--- a/crates/hir-ty/src/tests/regression.rs
+++ b/crates/hir-ty/src/tests/regression.rs
@@ -2363,6 +2363,7 @@ fn test() {
 }
 "#,
         expect![[r#"
+            46..49 'Foo': Foo<N>
             93..97 'self': Foo<N>
             108..125 '{     ...     }': usize
             118..119 'N': usize