about summary refs log tree commit diff
path: root/src/tools/rust-analyzer/crates/salsa/tests/cycles.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/rust-analyzer/crates/salsa/tests/cycles.rs')
-rw-r--r--src/tools/rust-analyzer/crates/salsa/tests/cycles.rs492
1 files changed, 492 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/salsa/tests/cycles.rs b/src/tools/rust-analyzer/crates/salsa/tests/cycles.rs
new file mode 100644
index 00000000000..ea5d15a250f
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/salsa/tests/cycles.rs
@@ -0,0 +1,492 @@
+use std::panic::UnwindSafe;
+
+use expect_test::expect;
+use salsa::{Durability, ParallelDatabase, Snapshot};
+
+// Axes:
+//
+// Threading
+// * Intra-thread
+// * Cross-thread -- part of cycle is on one thread, part on another
+//
+// Recovery strategies:
+// * Panic
+// * Fallback
+// * Mixed -- multiple strategies within cycle participants
+//
+// Across revisions:
+// * N/A -- only one revision
+// * Present in new revision, not old
+// * Present in old revision, not new
+// * Present in both revisions
+//
+// Dependencies
+// * Tracked
+// * Untracked -- cycle participant(s) contain untracked reads
+//
+// Layers
+// * Direct -- cycle participant is directly invoked from test
+// * Indirect -- invoked a query that invokes the cycle
+//
+//
+// | Thread | Recovery | Old, New | Dep style | Layers   | Test Name      |
+// | ------ | -------- | -------- | --------- | ------   | ---------      |
+// | Intra  | Panic    | N/A      | Tracked   | direct   | cycle_memoized |
+// | Intra  | Panic    | N/A      | Untracked | direct   | cycle_volatile |
+// | Intra  | Fallback | N/A      | Tracked   | direct   | cycle_cycle  |
+// | Intra  | Fallback | N/A      | Tracked   | indirect | inner_cycle |
+// | Intra  | Fallback | Both     | Tracked   | direct   | cycle_revalidate |
+// | Intra  | Fallback | New      | Tracked   | direct   | cycle_appears |
+// | Intra  | Fallback | Old      | Tracked   | direct   | cycle_disappears |
+// | Intra  | Fallback | Old      | Tracked   | direct   | cycle_disappears_durability |
+// | Intra  | Mixed    | N/A      | Tracked   | direct   | cycle_mixed_1 |
+// | Intra  | Mixed    | N/A      | Tracked   | direct   | cycle_mixed_2 |
+// | Cross  | Fallback | N/A      | Tracked   | both     | parallel/cycles.rs: recover_parallel_cycle |
+// | Cross  | Panic    | N/A      | Tracked   | both     | parallel/cycles.rs: panic_parallel_cycle |
+
+#[derive(PartialEq, Eq, Hash, Clone, Debug)]
+struct Error {
+    cycle: Vec<String>,
+}
+
+#[salsa::database(GroupStruct)]
+#[derive(Default)]
+struct DatabaseImpl {
+    storage: salsa::Storage<Self>,
+}
+
+impl salsa::Database for DatabaseImpl {}
+
+impl ParallelDatabase for DatabaseImpl {
+    fn snapshot(&self) -> Snapshot<Self> {
+        Snapshot::new(DatabaseImpl { storage: self.storage.snapshot() })
+    }
+}
+
+/// The queries A, B, and C in `Database` can be configured
+/// to invoke one another in arbitrary ways using this
+/// enum.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum CycleQuery {
+    None,
+    A,
+    B,
+    C,
+    AthenC,
+}
+
+#[salsa::query_group(GroupStruct)]
+trait Database: salsa::Database {
+    // `a` and `b` depend on each other and form a cycle
+    fn memoized_a(&self) -> ();
+    fn memoized_b(&self) -> ();
+    fn volatile_a(&self) -> ();
+    fn volatile_b(&self) -> ();
+
+    #[salsa::input]
+    fn a_invokes(&self) -> CycleQuery;
+
+    #[salsa::input]
+    fn b_invokes(&self) -> CycleQuery;
+
+    #[salsa::input]
+    fn c_invokes(&self) -> CycleQuery;
+
+    #[salsa::cycle(recover_a)]
+    fn cycle_a(&self) -> Result<(), Error>;
+
+    #[salsa::cycle(recover_b)]
+    fn cycle_b(&self) -> Result<(), Error>;
+
+    fn cycle_c(&self) -> Result<(), Error>;
+}
+
+fn recover_a(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
+    Err(Error { cycle: cycle.all_participants(db) })
+}
+
+fn recover_b(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
+    Err(Error { cycle: cycle.all_participants(db) })
+}
+
+fn memoized_a(db: &dyn Database) {
+    db.memoized_b()
+}
+
+fn memoized_b(db: &dyn Database) {
+    db.memoized_a()
+}
+
+fn volatile_a(db: &dyn Database) {
+    db.salsa_runtime().report_untracked_read();
+    db.volatile_b()
+}
+
+fn volatile_b(db: &dyn Database) {
+    db.salsa_runtime().report_untracked_read();
+    db.volatile_a()
+}
+
+impl CycleQuery {
+    fn invoke(self, db: &dyn Database) -> Result<(), Error> {
+        match self {
+            CycleQuery::A => db.cycle_a(),
+            CycleQuery::B => db.cycle_b(),
+            CycleQuery::C => db.cycle_c(),
+            CycleQuery::AthenC => {
+                let _ = db.cycle_a();
+                db.cycle_c()
+            }
+            CycleQuery::None => Ok(()),
+        }
+    }
+}
+
+fn cycle_a(db: &dyn Database) -> Result<(), Error> {
+    db.a_invokes().invoke(db)
+}
+
+fn cycle_b(db: &dyn Database) -> Result<(), Error> {
+    db.b_invokes().invoke(db)
+}
+
+fn cycle_c(db: &dyn Database) -> Result<(), Error> {
+    db.c_invokes().invoke(db)
+}
+
+#[track_caller]
+fn extract_cycle(f: impl FnOnce() + UnwindSafe) -> salsa::Cycle {
+    let v = std::panic::catch_unwind(f);
+    if let Err(d) = &v {
+        if let Some(cycle) = d.downcast_ref::<salsa::Cycle>() {
+            return cycle.clone();
+        }
+    }
+    panic!("unexpected value: {:?}", v)
+}
+
+#[test]
+fn cycle_memoized() {
+    let db = DatabaseImpl::default();
+    let cycle = extract_cycle(|| db.memoized_a());
+    expect![[r#"
+        [
+            "cycles::MemoizedAQuery::memoized_a(())",
+            "cycles::MemoizedBQuery::memoized_b(())",
+        ]
+    "#]]
+    .assert_debug_eq(&cycle.unexpected_participants(&db));
+}
+
+#[test]
+fn cycle_volatile() {
+    let db = DatabaseImpl::default();
+    let cycle = extract_cycle(|| db.volatile_a());
+    expect![[r#"
+        [
+            "cycles::VolatileAQuery::volatile_a(())",
+            "cycles::VolatileBQuery::volatile_b(())",
+        ]
+    "#]]
+    .assert_debug_eq(&cycle.unexpected_participants(&db));
+}
+
+#[test]
+fn cycle_cycle() {
+    let mut query = DatabaseImpl::default();
+
+    //     A --> B
+    //     ^     |
+    //     +-----+
+
+    query.set_a_invokes(CycleQuery::B);
+    query.set_b_invokes(CycleQuery::A);
+
+    assert!(query.cycle_a().is_err());
+}
+
+#[test]
+fn inner_cycle() {
+    let mut query = DatabaseImpl::default();
+
+    //     A --> B <-- C
+    //     ^     |
+    //     +-----+
+
+    query.set_a_invokes(CycleQuery::B);
+    query.set_b_invokes(CycleQuery::A);
+    query.set_c_invokes(CycleQuery::B);
+
+    let err = query.cycle_c();
+    assert!(err.is_err());
+    let cycle = err.unwrap_err().cycle;
+    expect![[r#"
+        [
+            "cycles::CycleAQuery::cycle_a(())",
+            "cycles::CycleBQuery::cycle_b(())",
+        ]
+    "#]]
+    .assert_debug_eq(&cycle);
+}
+
+#[test]
+fn cycle_revalidate() {
+    let mut db = DatabaseImpl::default();
+
+    //     A --> B
+    //     ^     |
+    //     +-----+
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::A);
+
+    assert!(db.cycle_a().is_err());
+    db.set_b_invokes(CycleQuery::A); // same value as default
+    assert!(db.cycle_a().is_err());
+}
+
+#[test]
+fn cycle_revalidate_unchanged_twice() {
+    let mut db = DatabaseImpl::default();
+
+    //     A --> B
+    //     ^     |
+    //     +-----+
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::A);
+
+    assert!(db.cycle_a().is_err());
+    db.set_c_invokes(CycleQuery::A); // force new revisi5on
+
+    // on this run
+    expect![[r#"
+        Err(
+            Error {
+                cycle: [
+                    "cycles::CycleAQuery::cycle_a(())",
+                    "cycles::CycleBQuery::cycle_b(())",
+                ],
+            },
+        )
+    "#]]
+    .assert_debug_eq(&db.cycle_a());
+}
+
+#[test]
+fn cycle_appears() {
+    let mut db = DatabaseImpl::default();
+
+    //     A --> B
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::None);
+    assert!(db.cycle_a().is_ok());
+
+    //     A --> B
+    //     ^     |
+    //     +-----+
+    db.set_b_invokes(CycleQuery::A);
+    tracing::debug!("Set Cycle Leaf");
+    assert!(db.cycle_a().is_err());
+}
+
+#[test]
+fn cycle_disappears() {
+    let mut db = DatabaseImpl::default();
+
+    //     A --> B
+    //     ^     |
+    //     +-----+
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::A);
+    assert!(db.cycle_a().is_err());
+
+    //     A --> B
+    db.set_b_invokes(CycleQuery::None);
+    assert!(db.cycle_a().is_ok());
+}
+
+/// A variant on `cycle_disappears` in which the values of
+/// `a_invokes` and `b_invokes` are set with durability values.
+/// If we are not careful, this could cause us to overlook
+/// the fact that the cycle will no longer occur.
+#[test]
+fn cycle_disappears_durability() {
+    let mut db = DatabaseImpl::default();
+    db.set_a_invokes_with_durability(CycleQuery::B, Durability::LOW);
+    db.set_b_invokes_with_durability(CycleQuery::A, Durability::HIGH);
+
+    let res = db.cycle_a();
+    assert!(res.is_err());
+
+    // At this point, `a` read `LOW` input, and `b` read `HIGH` input. However,
+    // because `b` participates in the same cycle as `a`, its final durability
+    // should be `LOW`.
+    //
+    // Check that setting a `LOW` input causes us to re-execute `b` query, and
+    // observe that the cycle goes away.
+    db.set_a_invokes_with_durability(CycleQuery::None, Durability::LOW);
+
+    let res = db.cycle_b();
+    assert!(res.is_ok());
+}
+
+#[test]
+fn cycle_mixed_1() {
+    let mut db = DatabaseImpl::default();
+    //     A --> B <-- C
+    //           |     ^
+    //           +-----+
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::C);
+    db.set_c_invokes(CycleQuery::B);
+
+    let u = db.cycle_c();
+    expect![[r#"
+        Err(
+            Error {
+                cycle: [
+                    "cycles::CycleBQuery::cycle_b(())",
+                    "cycles::CycleCQuery::cycle_c(())",
+                ],
+            },
+        )
+    "#]]
+    .assert_debug_eq(&u);
+}
+
+#[test]
+fn cycle_mixed_2() {
+    let mut db = DatabaseImpl::default();
+
+    // Configuration:
+    //
+    //     A --> B --> C
+    //     ^           |
+    //     +-----------+
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::C);
+    db.set_c_invokes(CycleQuery::A);
+
+    let u = db.cycle_a();
+    expect![[r#"
+        Err(
+            Error {
+                cycle: [
+                    "cycles::CycleAQuery::cycle_a(())",
+                    "cycles::CycleBQuery::cycle_b(())",
+                    "cycles::CycleCQuery::cycle_c(())",
+                ],
+            },
+        )
+    "#]]
+    .assert_debug_eq(&u);
+}
+
+#[test]
+fn cycle_deterministic_order() {
+    // No matter whether we start from A or B, we get the same set of participants:
+    let db = || {
+        let mut db = DatabaseImpl::default();
+        //     A --> B
+        //     ^     |
+        //     +-----+
+        db.set_a_invokes(CycleQuery::B);
+        db.set_b_invokes(CycleQuery::A);
+        db
+    };
+    let a = db().cycle_a();
+    let b = db().cycle_b();
+    expect![[r#"
+        (
+            Err(
+                Error {
+                    cycle: [
+                        "cycles::CycleAQuery::cycle_a(())",
+                        "cycles::CycleBQuery::cycle_b(())",
+                    ],
+                },
+            ),
+            Err(
+                Error {
+                    cycle: [
+                        "cycles::CycleAQuery::cycle_a(())",
+                        "cycles::CycleBQuery::cycle_b(())",
+                    ],
+                },
+            ),
+        )
+    "#]]
+    .assert_debug_eq(&(a, b));
+}
+
+#[test]
+fn cycle_multiple() {
+    // No matter whether we start from A or B, we get the same set of participants:
+    let mut db = DatabaseImpl::default();
+
+    // Configuration:
+    //
+    //     A --> B <-- C
+    //     ^     |     ^
+    //     +-----+     |
+    //           |     |
+    //           +-----+
+    //
+    // Here, conceptually, B encounters a cycle with A and then
+    // recovers.
+    db.set_a_invokes(CycleQuery::B);
+    db.set_b_invokes(CycleQuery::AthenC);
+    db.set_c_invokes(CycleQuery::B);
+
+    let c = db.cycle_c();
+    let b = db.cycle_b();
+    let a = db.cycle_a();
+    expect![[r#"
+        (
+            Err(
+                Error {
+                    cycle: [
+                        "cycles::CycleAQuery::cycle_a(())",
+                        "cycles::CycleBQuery::cycle_b(())",
+                    ],
+                },
+            ),
+            Err(
+                Error {
+                    cycle: [
+                        "cycles::CycleAQuery::cycle_a(())",
+                        "cycles::CycleBQuery::cycle_b(())",
+                    ],
+                },
+            ),
+            Err(
+                Error {
+                    cycle: [
+                        "cycles::CycleAQuery::cycle_a(())",
+                        "cycles::CycleBQuery::cycle_b(())",
+                    ],
+                },
+            ),
+        )
+    "#]]
+    .assert_debug_eq(&(a, b, c));
+}
+
+#[test]
+fn cycle_recovery_set_but_not_participating() {
+    let mut db = DatabaseImpl::default();
+
+    //     A --> C -+
+    //           ^  |
+    //           +--+
+    db.set_a_invokes(CycleQuery::C);
+    db.set_c_invokes(CycleQuery::C);
+
+    // Here we expect C to panic and A not to recover:
+    let r = extract_cycle(|| drop(db.cycle_a()));
+    expect![[r#"
+        [
+            "cycles::CycleCQuery::cycle_c(())",
+        ]
+    "#]]
+    .assert_debug_eq(&r.all_participants(&db));
+}