#![cfg(feature = "inventory")]
mod common;
use common::LogDatabase;
use expect_test::expect;
use salsa::{Database, Durability, HashEqLike, Lookup, Setter};
use test_log::test;
#[salsa::input]
struct Input {
#[returns(copy)]
field1: usize,
}
#[salsa::interned(revisions = 3)]
#[derive(Debug)]
struct Interned<'db> {
field1: BadHash,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, salsa::SalsaValue)]
struct BadHash(usize);
impl std::hash::Hash for BadHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_i16(0);
}
}
#[derive(Debug)]
struct PanickingLookup(usize);
impl std::hash::Hash for PanickingLookup {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_i16(0);
}
}
impl Lookup<BadHash> for PanickingLookup {
fn into_owned(self) -> BadHash {
assert_ne!(self.0, 2, "lookup panic");
BadHash(self.0)
}
}
impl HashEqLike<PanickingLookup> for BadHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_i16(0);
}
fn eq(&self, data: &PanickingLookup) -> bool {
self.0 == data.0
}
}
#[salsa::interned(revisions = 1)]
struct PanickingInterned<'db> {
value: BadHash,
}
#[salsa::tracked(returns(copy))]
fn intern_panicking(db: &dyn Database, input: Input) -> PanickingInterned<'_> {
PanickingInterned::new(db, PanickingLookup(input.field1(db)))
}
#[test]
fn panic_during_reuse_does_not_orphan_slot() {
use std::panic::{AssertUnwindSafe, catch_unwind};
use salsa::plumbing::AsId;
let mut db = common::LoggerDatabase::default();
let input = Input::new(&db, 0);
let first_id = intern_panicking(&db, input).as_id();
input.set_field1(&mut db).to(1);
let second_id = intern_panicking(&db, input).as_id();
assert_eq!(second_id, first_id.next_generation().unwrap());
input.set_field1(&mut db).to(2);
let result = catch_unwind(AssertUnwindSafe(|| intern_panicking(&db, input)));
assert!(result.is_err());
input.set_field1(&mut db).to(3);
let recovered_id = intern_panicking(&db, input).as_id();
assert_eq!(recovered_id, second_id.next_generation().unwrap());
}
#[salsa::interned]
#[derive(Debug)]
struct NestedInterned<'db> {
#[returns(copy)]
interned: Interned<'db>,
}
#[test]
fn test_intern_new() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result_in_rev_1 = function(&db, input);
assert_eq!(result_in_rev_1.field1(&db).0, 0);
input.set_field1(&mut db).to(1);
let result_in_rev_2 = function(&db, input);
assert_eq!(result_in_rev_2.field1(&db).0, 1);
db.assert_logs(expect![[r#"
[
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(80)), revision: R1 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(81)), revision: R2 }",
]"#]]);
}
#[test]
fn test_reintern() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Interned<'_> {
let _ = input.field1(db);
Interned::new(db, BadHash(0))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result_in_rev_1 = function(&db, input);
db.assert_logs(expect![[r#"
[
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(80)), revision: R1 }",
]"#]]);
assert_eq!(result_in_rev_1.field1(&db).0, 0);
input.set_field1(&mut db).to(1);
let result_in_rev_2 = function(&db, input);
db.assert_logs(expect![[r#"
[
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidValidateInternedValue { key: Interned(Id(80)), revision: R2 }",
]"#]]);
assert_eq!(result_in_rev_2.field1(&db).0, 0);
}
#[test]
fn test_durability() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, _input: Input) -> Interned<'_> {
Interned::new(db, BadHash(0))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result_in_rev_1 = function(&db, input);
assert_eq!(result_in_rev_1.field1(&db).0, 0);
input.set_field1(&mut db).to(1);
let result_in_rev_2 = function(&db, input);
assert_eq!(result_in_rev_2.field1(&db).0, 0);
db.assert_logs(expect![[r#"
[
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(80)), revision: R1 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"DidValidateMemoizedValue { database_key: function(Id(0)) }",
]"#]]);
}
#[test]
fn test_non_reusable_new_value_does_not_record_dependency() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Interned<'_> {
let _ = input.field1(db);
Interned::new(db, BadHash(0))
}
for durability in [Durability::MEDIUM, Durability::HIGH] {
let mut db = common::EventLoggerDatabase::default();
let input = Input::builder(0).durability(durability).new(&db);
let _ = function(&db, input);
db.clear_logs();
db.synthetic_write(durability);
let _ = function(&db, input);
db.assert_logs(expect![[r#"
[
"DidSetCancellationFlag",
"WillCheckCancellation",
"DidValidateMemoizedValue { database_key: function(Id(0)) }",
]"#]]);
}
}
#[test]
fn test_non_reusable_existing_value_does_not_record_dependency() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Interned<'_> {
let _ = input.field1(db);
Interned::new(db, BadHash(0))
}
for durability in [Durability::MEDIUM, Durability::HIGH] {
let mut db = common::EventLoggerDatabase::default();
let input0 = Input::builder(0).durability(durability).new(&db);
let input1 = Input::builder(0).durability(durability).new(&db);
let _ = function(&db, input0);
let _ = function(&db, input1);
db.clear_logs();
db.synthetic_write(durability);
let _ = function(&db, input1);
db.assert_logs(expect![[r#"
[
"DidSetCancellationFlag",
"WillCheckCancellation",
"DidValidateMemoizedValue { database_key: function(Id(1)) }",
]"#]]);
}
}
#[test]
fn test_non_reusable_value_still_updates_query_stamp() {
#[salsa::tracked(returns(copy))]
fn outer(db: &dyn Database, input: Input) -> bool {
let _ = input.field1(db);
let _ = Interned::new(db, BadHash(0));
true
}
#[salsa::tracked(returns(copy))]
fn intern_from_input(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
db.synthetic_write(Durability::LOW);
assert!(outer(&db, input));
for key in 1..10 {
db.synthetic_write(Durability::LOW);
let _ = intern_from_input(&db, Input::new(&db, key));
}
let _ = Interned::new(&db, BadHash(0));
assert!(outer(&db, input));
}
#[salsa::interned(revisions = usize::MAX)]
#[derive(Debug)]
struct Immortal<'db> {
field1: BadHash,
}
#[salsa::interned(revisions = 4)]
#[derive(Debug)]
struct SpilledInterned<'db> {
field1: BadHash,
}
#[test]
fn test_revisions_above_inline_capacity() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> SpilledInterned<'_> {
SpilledInterned::new(db, BadHash(input.field1(db)))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, 0);
assert_eq!(salsa::plumbing::AsId::as_id(&result).generation(), 0);
for i in 1..4 {
input.set_field1(&mut db).to(i);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, i);
assert_eq!(salsa::plumbing::AsId::as_id(&result).generation(), 0);
}
input.set_field1(&mut db).to(4);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, 4);
assert_eq!(salsa::plumbing::AsId::as_id(&result).generation(), 1);
}
#[test]
fn test_immortal() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Immortal<'_> {
Immortal::new(db, BadHash(input.field1(db)))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, 0);
for i in 1..if cfg!(miri) { 50 } else { 1000 } {
input.set_field1(&mut db).to(i);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, i);
assert_eq!(salsa::plumbing::AsId::as_id(&result).generation(), 0);
}
}
#[test]
fn test_reuse() {
#[salsa::tracked(returns(copy))]
fn function(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, 0);
for i in 1..10 {
input.set_field1(&mut db).to(i);
let result = function(&db, input);
assert_eq!(result.field1(&db).0, i);
}
for i in 1..10 {
let result = function(&db, Input::new(&db, i));
assert_eq!(result.field1(&db).0, i);
}
db.assert_logs(expect![[r#"
[
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(80)), revision: R1 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(81)), revision: R2 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidInternValue { key: Interned(Id(82)), revision: R3 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(80g1)), revision: R4 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(81g1)), revision: R5 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(82g1)), revision: R6 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(80g2)), revision: R7 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(81g2)), revision: R8 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(82g2)), revision: R9 }",
"DidSetCancellationFlag",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(0)) }",
"DidReuseInternedValue { key: Interned(Id(80g3)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(1)) }",
"DidInternValue { key: Interned(Id(83)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(2)) }",
"DidInternValue { key: Interned(Id(84)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(3)) }",
"DidInternValue { key: Interned(Id(85)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(4)) }",
"DidInternValue { key: Interned(Id(86)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(5)) }",
"DidInternValue { key: Interned(Id(87)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(6)) }",
"DidInternValue { key: Interned(Id(88)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(7)) }",
"DidValidateInternedValue { key: Interned(Id(81g2)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(8)) }",
"DidValidateInternedValue { key: Interned(Id(82g2)), revision: R10 }",
"WillCheckCancellation",
"WillExecute { database_key: function(Id(9)) }",
]"#]]);
}
#[test]
fn reuse_discards_memos_for_old_generation() {
use salsa::plumbing::AsId;
#[salsa::tracked(returns(copy))]
fn intern(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
#[salsa::tracked(returns(copy))]
fn read(db: &dyn Database, interned: Interned<'_>) -> usize {
interned.field1(db).0
}
let mut db = common::DiscardLoggerDatabase::default();
let input = Input::new(&db, 0);
let first = intern(&db, input);
let first_id = first.as_id();
assert_eq!(read(&db, first), 0);
db.clear_logs();
let mut reused = false;
for value in 1..10 {
input.set_field1(&mut db).to(value);
if intern(&db, input).as_id().index() == first_id.index() {
reused = true;
break;
}
}
assert!(reused);
db.assert_logs(expect![[r#"
[
"salsa_event(DidDiscard { key: read(Id(80)) })",
]"#]]);
}
#[test]
fn test_reuse_indirect() {
#[salsa::tracked(returns(copy))]
fn intern(db: &dyn Database, input: Input, value: usize) -> Interned<'_> {
intern_inner(db, input, value)
}
#[salsa::tracked(returns(copy))]
fn intern_inner(db: &dyn Database, input: Input, value: usize) -> Interned<'_> {
let _i = input.field1(db); Interned::new(db, BadHash(value))
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::builder(0).durability(Durability::LOW).new(&db);
let i0 = intern(&db, input, 0);
let i0_id = salsa::plumbing::AsId::as_id(&i0);
assert_eq!(i0.field1(&db).0, 0);
for x in 1.. {
db.synthetic_write(Durability::LOW);
let ix = intern(&db, input, x);
let ix_id = salsa::plumbing::AsId::as_id(&ix);
if ix_id.index() == i0_id.index() {
assert_eq!(ix.field1(&db).0, x);
let i0 = intern(&db, input, 0);
assert_eq!(i0.field1(&db).0, 0);
break;
}
}
}
#[test]
fn test_reuse_interned_input() {
#[salsa::tracked(returns(copy))]
fn create_interned(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
#[salsa::tracked(returns(copy))]
fn use_interned<'db>(db: &'db dyn Database, interned: Interned<'db>) -> usize {
interned.field1(db).0
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let interned = create_interned(&db, input);
let result = use_interned(&db, interned);
assert_eq!(result, 0);
input.set_field1(&mut db).to(1);
for _ in 0..10 {
let interned = create_interned(&db, input);
let result = use_interned(&db, interned);
assert_eq!(result, 1);
input.set_field1(&mut db).to(1);
}
input.set_field1(&mut db).to(2);
let interned = create_interned(&db, input);
let result = use_interned(&db, interned);
assert_eq!(result, 2);
}
#[test]
fn test_reuse_multiple_interned_input() {
#[salsa::tracked(returns(copy))]
fn create_interned(db: &dyn Database, input: Input) -> Interned<'_> {
Interned::new(db, BadHash(input.field1(db)))
}
#[salsa::tracked(returns(copy))]
fn create_nested_interned<'db>(
db: &'db dyn Database,
interned: Interned<'db>,
) -> NestedInterned<'db> {
NestedInterned::new(db, interned)
}
#[salsa::tracked(returns(copy))]
fn use_interned<'db>(db: &'db dyn Database, interned: Interned<'db>) -> usize {
interned.field1(db).0
}
#[salsa::tracked(returns(copy))]
fn use_nested_interned<'db>(
db: &'db dyn Database,
nested_interned: NestedInterned<'db>,
) -> usize {
nested_interned.interned(db).field1(db).0
}
let mut db = common::EventLoggerDatabase::default();
let input = Input::new(&db, 0);
let interned = create_interned(&db, input);
let i0_id = salsa::plumbing::AsId::as_id(&interned);
let nested_interned = create_nested_interned(&db, interned);
let result = use_nested_interned(&db, nested_interned);
assert_eq!(result, 0);
input.set_field1(&mut db).to(1);
for _ in 0..10 {
let interned = create_interned(&db, input);
let result = use_interned(&db, interned);
assert_eq!(result, 1);
input.set_field1(&mut db).to(1);
}
input.set_field1(&mut db).to(2);
let interned = create_interned(&db, input);
let i2_id = salsa::plumbing::AsId::as_id(&interned);
assert_ne!(i0_id, i2_id);
let nested_interned = create_nested_interned(&db, interned);
let result = use_nested_interned(&db, nested_interned);
assert_eq!(result, 2);
}
#[test]
fn test_durability_increase() {
#[salsa::tracked(returns(copy))]
fn intern(db: &dyn Database, input: Input, value: usize) -> Interned<'_> {
let _f = input.field1(db);
Interned::new(db, BadHash(value))
}
let mut db = common::EventLoggerDatabase::default();
let high_durability = Input::builder(0).durability(Durability::HIGH).new(&db);
let low_durability = Input::builder(1).durability(Durability::LOW).new(&db);
let _i0 = intern(&db, low_durability, 0);
let _i0 = intern(&db, high_durability, 0);
for _ in 0..100 {
let _dummy = intern(&db, low_durability, 1000).field1(&db);
db.synthetic_write(Durability::LOW);
}
let _i1 = intern(&db, low_durability, 1);
let value = intern(&db, high_durability, 0);
assert_eq!(value.field1(&db).0, 0);
db.synthetic_write(Durability::LOW);
let value = intern(&db, high_durability, 0);
assert_eq!(value.field1(&db).0, 0);
}