Repository: salsa-rs/salsa (Rust)

Issue:
Crash with BadHash input and multiple changes
```rust 
#[salsa::input]
    struct TmpInput {
        pub id: u64,
    }

    #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
    struct BadHash {
        field: u64,
    }

    impl From<u64> for BadHash {
        fn from(value: u64) -> Self {
            Self { field: value }
        }
    }

    impl std::hash::Hash for BadHash {
        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
            state.write_i16(0);
        }
    }

    #[salsa::tracked(debug)]
    struct Tmp<'db> {
        id: BadHash,
    }

    #[salsa::tracked]
    fn tmp_getter<'db>(db: &'db dyn base_db::BaseDb, input: TmpInput) -> Tmp<'db> {
        Tmp::new(db, input.id(db).into())
    }

    #[test]
    fn generations_test() {
        tracing_subscriber::fmt().init();
        let mut db = TestDatabase::default();
        let input = TmpInput::new(&db, 9);
        dbg!(tmp_getter(&db, input).0);
        input.set_id(&mut db).to(10);
        dbg!(tmp_getter(&db, input).0);
        input.set_id(&mut db).to(9);
        dbg!(tmp_getter(&db, input).0);
    }
```
<details>
<summary>Logs</summary>

```
---- tests::eq_ord_test stdout ----
2025-05-25T07:15:37.710347Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
2025-05-25T07:15:37.710557Z  INFO salsa::function::execute: tmp_getter(Id(400)): executing query
2025-05-25T07:15:37.710641Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillExecute { database_key: tmp_getter(Id(400)) } }
2025-05-25T07:15:37.710756Z DEBUG salsa::zalsa_local: report_tracked_read(input=TmpInput.id(Id(400)), durability=Durability(0), changed_at=R1)
2025-05-25T07:15:37.711036Z DEBUG salsa::zalsa_local: report_tracked_read(input=tmp_getter(Id(400)), durability=Durability(0), changed_at=R1)
2025-05-25T07:15:37.711137Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: DidSetCancellationFlag }
2025-05-25T07:15:37.711210Z DEBUG salsa::runtime: new_revision: R1 -> R2
2025-05-25T07:15:37.711326Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
2025-05-25T07:15:37.711420Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): shallow_verify_memo(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 1,
    },
})
2025-05-25T07:15:37.711631Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): check_durability(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 1,
    },
}, last_changed=R2 <= verified_at=R1) = false
2025-05-25T07:15:37.711786Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): deep_verify_memo(old_memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 1,
    },
})
2025-05-25T07:15:37.711929Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): shallow_verify_memo(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 1,
    },
})
2025-05-25T07:15:37.712080Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): check_durability(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 1,
    },
}, last_changed=R2 <= verified_at=R1) = false
2025-05-25T07:15:37.712230Z  INFO salsa::function::execute: tmp_getter(Id(400)): executing query
2025-05-25T07:15:37.712315Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillExecute { database_key: tmp_getter(Id(400)) } }
2025-05-25T07:15:37.712430Z DEBUG salsa::zalsa_local: report_tracked_read(input=TmpInput.id(Id(400)), durability=Durability(0), changed_at=R2)
2025-05-25T07:15:37.712643Z DEBUG salsa::zalsa_local: report_tracked_read(input=tmp_getter(Id(400)), durability=Durability(0), changed_at=R2)
2025-05-25T07:15:37.712744Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: DidSetCancellationFlag }
2025-05-25T07:15:37.712843Z DEBUG salsa::runtime: new_revision: R2 -> R3
2025-05-25T07:15:37.712966Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillCheckCancellation }
2025-05-25T07:15:37.713072Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): shallow_verify_memo(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 2,
    },
})
2025-05-25T07:15:37.713212Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): check_durability(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 2,
    },
}, last_changed=R3 <= verified_at=R2) = false
2025-05-25T07:15:37.713344Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): deep_verify_memo(old_memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 2,
    },
})
2025-05-25T07:15:37.713485Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): shallow_verify_memo(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 2,
    },
})
2025-05-25T07:15:37.713634Z DEBUG salsa::function::maybe_changed_after: tmp_getter(Id(400)): check_durability(memo = Memo {
    value: "Some(<value>)",
    verified_at: AtomicRevision {
        data: 2,
    },
}, last_changed=R3 <= verified_at=R2) = false
2025-05-25T07:15:37.713784Z  INFO salsa::function::execute: tmp_getter(Id(400)): executing query
2025-05-25T07:15:37.713856Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillExecute { database_key: tmp_getter(Id(400)) } }
2025-05-25T07:15:37.713942Z DEBUG salsa::zalsa_local: report_tracked_read(input=TmpInput.id(Id(400)), durability=Durability(0), changed_at=R3)
2025-05-25T07:15:37.714096Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: WillDiscardStaleOutput { execute_key: tmp_getter(Id(400)), output_key: Tmp(Id(800)) } }
2025-05-25T07:15:37.714214Z DEBUG base_db: Event: Event { thread_id: ThreadId(2), kind: DidDiscard { key: Tmp(Id(800)) } }

thread 'resolver::tests::eq_ord_test' panicked at C:\Users\user\.cargo\git\checkouts\salsa-e6f3bb7c2a062968\0414d89\src\tracked_struct.rs:652:53:
cannot delete read-locked id `Id(800)`; value leaked across threads or user functions not deterministic
stack backtrace:
```

</details>


Discussion:
Thanks for the test case. Is this on main or the latest stable version?
> Thanks for the test case. Is this on main or the latest stable version?

fails on master branch, works fine on 0.22
Seems like swap_remove in diff_outputs should ignore output generation?



Patch:
diff --git a/src/function/diff_outputs.rs b/src/function/diff_outputs.rs
index b00dc4143..319e8d4ec 100644
--- a/src/function/diff_outputs.rs
+++ b/src/function/diff_outputs.rs
@@ -3,7 +3,7 @@ use crate::function::{Configuration, IngredientImpl};
 use crate::hash::FxIndexSet;
 use crate::zalsa::Zalsa;
 use crate::zalsa_local::QueryRevisions;
-use crate::{DatabaseKeyIndex, Event, EventKind};
+use crate::{DatabaseKeyIndex, Event, EventKind, Id};
 
 impl<C> IngredientImpl<C>
 where
@@ -14,9 +14,6 @@ where
     ///
     /// This function takes a `&mut` reference to `revisions` to remove outputs
     /// that no longer exist in this revision from [`QueryRevisions::tracked_struct_ids`].
-    ///
-    /// If `provisional` is true, the new outputs are from a cycle-provisional result. In
-    /// that case, we won't panic if we see outputs from the current revision become stale.
     pub(super) fn diff_outputs(
         &self,
         zalsa: &Zalsa,
@@ -25,7 +22,15 @@ where
         revisions: &mut QueryRevisions,
     ) {
         // Iterate over the outputs of the `old_memo` and put them into a hashset
-        let mut old_outputs: FxIndexSet<_> = old_memo.revisions.origin.outputs().collect();
+        //
+        // Ignore key_generation here, because we use the same tracked struct allocation for
+        // all generations with the same key_index and can't report it as stale
+        let mut old_outputs: FxIndexSet<_> = old_memo
+            .revisions
+            .origin
+            .outputs()
+            .map(|a| (a.ingredient_index(), a.key_index().index()))
+            .collect();
 
         if old_outputs.is_empty() {
             return;
@@ -34,7 +39,10 @@ where
         // Iterate over the outputs of the current query
         // and remove elements from `old_outputs` when we find them
         for new_output in revisions.origin.outputs() {
-            old_outputs.swap_remove(&new_output);
+            old_outputs.swap_remove(&(
+                new_output.ingredient_index(),
+                new_output.key_index().index(),
+            ));
         }
 
         if old_outputs.is_empty() {
@@ -43,12 +51,14 @@ where
 
         // Remove the outputs that are no longer present in the current revision
         // to prevent that the next revision is seeded with an id mapping that no longer exists.
-        revisions.tracked_struct_ids.retain(|&k, &mut value| {
-            !old_outputs.contains(&DatabaseKeyIndex::new(k.ingredient_index(), value))
-        });
+        revisions
+            .tracked_struct_ids
+            .retain(|&k, &mut value| !old_outputs.contains(&(k.ingredient_index(), value.index())));
 
-        for old_output in old_outputs {
-            Self::report_stale_output(zalsa, key, old_output);
+        for (ingredient_index, key_index) in old_outputs {
+            // SAFETY: key_index acquired from valid output
+            let id = unsafe { Id::from_index(key_index) };
+            Self::report_stale_output(zalsa, key, DatabaseKeyIndex::new(ingredient_index, id));
         }
     }
 


Test patch:
diff --git a/tests/tracked-struct-id-field-bad-hash.rs b/tests/tracked-struct-id-field-bad-hash.rs
index 746a7cee2..f60aa4798 100644
--- a/tests/tracked-struct-id-field-bad-hash.rs
+++ b/tests/tracked-struct-id-field-bad-hash.rs
@@ -11,16 +11,16 @@ use test_log::test;
 
 #[salsa::input]
 struct MyInput {
-    field: bool,
+    field: u64,
 }
 
 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
 struct BadHash {
-    field: bool,
+    field: u64,
 }
 
-impl From<bool> for BadHash {
-    fn from(value: bool) -> Self {
+impl From<u64> for BadHash {
+    fn from(value: u64) -> Self {
         Self { field: value }
     }
 }
@@ -46,9 +46,9 @@ fn the_fn(db: &dyn Db, input: MyInput) {
 fn execute() {
     let mut db = salsa::DatabaseImpl::new();
 
-    let input = MyInput::new(&db, true);
+    let input = MyInput::new(&db, 1);
     the_fn(&db, input);
-    input.set_field(&mut db).to(false);
+    input.set_field(&mut db).to(0);
     the_fn(&db, input);
 }
 
@@ -58,7 +58,7 @@ fn create_tracked<'db>(db: &'db dyn Db, input: MyInput) -> MyTracked<'db> {
 }
 
 #[salsa::tracked]
-fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> bool {
+fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> u64 {
     tracked.field(db).field
 }
 
@@ -66,16 +66,19 @@ fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> bool {
 fn dependent_query() {
     let mut db = salsa::DatabaseImpl::new();
 
-    let input = MyInput::new(&db, true);
+    let input = MyInput::new(&db, 1);
     let tracked = create_tracked(&db, input);
-    assert!(with_tracked(&db, tracked));
+    assert_eq!(with_tracked(&db, tracked), 1);
 
-    input.set_field(&mut db).to(false);
+    input.set_field(&mut db).to(0);
 
     // We now re-run the query that creates the tracked struct.
     //
     // Salsa will re-use the `MyTracked` struct from the previous revision,
     // but practically it has been re-created due to generational ids.
     let tracked = create_tracked(&db, input);
-    assert!(!with_tracked(&db, tracked));
+    assert_eq!(with_tracked(&db, tracked), 0);
+    input.set_field(&mut db).to(2);
+    let tracked = create_tracked(&db, input);
+    assert_eq!(with_tracked(&db, tracked), 2);
 }