Skip to content

Commit 8398398

Browse files
committed
fix some bugs in deep_verify_memo
1 parent 20bc3b2 commit 8398398

File tree

2 files changed

+170
-9
lines changed

2 files changed

+170
-9
lines changed

src/function/maybe_changed_after.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ where
110110
return Some(VerifyResult::Unchanged(
111111
InputAccumulatedValues::Empty,
112112
FxHashSet::from_iter([database_key_index]),
113-
))
113+
));
114114
}
115115
},
116116
ClaimResult::Claimed(guard) => guard,
@@ -257,9 +257,6 @@ where
257257
if self.shallow_verify_memo(db, zalsa, database_key_index, old_memo, false) {
258258
return VerifyResult::Unchanged(InputAccumulatedValues::Empty, Default::default());
259259
}
260-
if old_memo.may_be_provisional() {
261-
return VerifyResult::Changed;
262-
}
263260

264261
let mut cycle_heads = FxHashSet::default();
265262
loop {
@@ -338,6 +335,42 @@ where
338335
}
339336
};
340337

338+
// If this was a provisional memo from an older revision, we should have now validated
339+
// the latest memos of all dependencies, so try one more time to validate ourselves;
340+
// otherwise return changed. (This is necessary because a provisional memo may have a
341+
// cycle head which itself is provisional, and in the previous revision it's possible
342+
// that neither one was ever finalized; `validate_provisional` is not recursive, so we
343+
// need to validate them in the right order.)
344+
if old_memo.may_be_provisional() && !self.validate_provisional(db, zalsa, &old_memo) {
345+
return VerifyResult::Changed;
346+
}
347+
348+
// Possible scenarios here:
349+
//
350+
// 1. Cycle heads is empty. We traversed our full dependency graph and neither hit any
351+
// cycles, nor found any changed dependencies. We can mark our memo verified and
352+
// return Unchanged with empty cycle heads.
353+
//
354+
// 2. Cycle heads is non-empty, and does not contain our own key index. We are part of
355+
// a cycle, and since we don't know if some other cycle participant that hasn't been
356+
// traversed yet (that is, some other dependency of the cycle head, which is only a
357+
// dependency of ours via the cycle) might still have changed, we can't yet mark our
358+
// memo verified. We can return a provisional Unchanged, with cycle heads.
359+
//
360+
// 3. Cycle heads is non-empty, and contains only our own key index. We are the head of
361+
// a cycle, and we've now traversed the entire cycle and found no changes, but no
362+
// other cycle participants were verified (they would have all hit case 2 above). We
363+
// can now safely mark our own memo as verified. Then we have to traverse the entire
364+
// cycle again. This time, since our own memo is verified, there will be no cycle
365+
// encountered, and the rest of the cycle will be able to verify itself.
366+
//
367+
// 4. Cycle heads is non-empty, and contains our own key index as well as other key
368+
// indices. We are the head of a cycle nested within another cycle. We can't mark
369+
// our own memo verified (for the same reason as in case 2: the full outer cycle
370+
// hasn't been validated unchanged yet). We return Unchanged, with ourself removed
371+
// from cycle heads. We will handle our own memo (and the rest of our cycle) on a
372+
// future iteration; first the outer cycle head needs to verify itself.
373+
341374
let in_heads = cycle_heads.remove(&database_key_index);
342375

343376
if cycle_heads.is_empty() {
@@ -347,10 +380,15 @@ where
347380
database_key_index,
348381
inputs,
349382
);
350-
}
351-
if in_heads {
352-
cycle_heads.clear();
353-
continue;
383+
384+
if in_heads {
385+
// Iterate our dependency graph again, starting from the top. We clear the
386+
// cycle heads here because we are starting a fresh traversal. (It might be
387+
// logically clearer to create a new HashSet each time, but clearing the
388+
// existing one is more efficient.)
389+
cycle_heads.clear();
390+
continue;
391+
}
354392
}
355393
return VerifyResult::Unchanged(InputAccumulatedValues::Empty, cycle_heads);
356394
}

tests/cycle.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,13 @@ fn nested_double_multiple_revisions() {
802802
a.clone().assert_bounds(&db);
803803

804804
// and next revision, we converge
805-
c_in.set_inputs(&mut db).to(vec![value(240), a.clone(), b]);
805+
c_in.set_inputs(&mut db)
806+
.to(vec![value(240), a.clone(), b.clone()]);
807+
808+
a.clone().assert_value(&db, 240);
809+
810+
// one more revision, without relevant changes
811+
a_in.set_inputs(&mut db).to(vec![b]);
806812

807813
a.assert_value(&db, 240);
808814
}
@@ -879,3 +885,120 @@ fn cycle_unchanged() {
879885

880886
a.assert_value(&db, 45);
881887
}
888+
889+
/// a:Np(v59, b) -> b:Ni(v60, c) -> c:Np(d) -> d:Ni(v61, b, e) -> e:Np(d)
890+
/// ^ | ^ |
891+
/// +--------------------------+ +--------------+
892+
///
893+
/// If nothing in a nested cycle changed in the new revision, no part of the cycle should
894+
/// re-execute.
895+
#[test]
896+
fn cycle_unchanged_nested() {
897+
let mut db = ExecuteValidateLoggerDatabase::default();
898+
let a_in = Inputs::new(&db, vec![]);
899+
let b_in = Inputs::new(&db, vec![]);
900+
let c_in = Inputs::new(&db, vec![]);
901+
let d_in = Inputs::new(&db, vec![]);
902+
let e_in = Inputs::new(&db, vec![]);
903+
let a = Input::MinPanic(a_in);
904+
let b = Input::MinIterate(b_in);
905+
let c = Input::MinPanic(c_in);
906+
let d = Input::MinIterate(d_in);
907+
let e = Input::MinPanic(e_in);
908+
a_in.set_inputs(&mut db).to(vec![value(59), b.clone()]);
909+
b_in.set_inputs(&mut db).to(vec![value(60), c.clone()]);
910+
c_in.set_inputs(&mut db).to(vec![d.clone()]);
911+
d_in.set_inputs(&mut db)
912+
.to(vec![value(61), b.clone(), e.clone()]);
913+
e_in.set_inputs(&mut db).to(vec![d.clone()]);
914+
915+
a.clone().assert_value(&db, 59);
916+
b.clone().assert_value(&db, 60);
917+
918+
db.assert_logs_len(15);
919+
920+
// next revision, we change only A, which is not part of the cycle and the cycle does not
921+
// depend on.
922+
a_in.set_inputs(&mut db).to(vec![value(45), b.clone()]);
923+
b.assert_value(&db, 60);
924+
925+
db.assert_logs(expect![[r#"
926+
[
927+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(1)) })",
928+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(3)) })",
929+
"salsa_event(DidValidateMemoizedValue { database_key: min_panic(Id(4)) })",
930+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(3)) })",
931+
"salsa_event(DidValidateMemoizedValue { database_key: min_panic(Id(2)) })",
932+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(1)) })",
933+
]"#]]);
934+
935+
a.assert_value(&db, 45);
936+
}
937+
938+
/// +--------------------------------+
939+
/// | v
940+
/// a:Np(v59, b) -> b:Ni(v60, c) -> c:Np(d, e) -> d:Ni(v61, b, e) -> e:Ni(d)
941+
/// ^ | ^ |
942+
/// +-----------------------------+ +--------------+
943+
///
944+
/// If nothing in a nested cycle changed in the new revision, no part of the cycle should
945+
/// re-execute.
946+
#[test_log::test]
947+
fn cycle_unchanged_nested_intertwined() {
948+
// We run this test twice in order to catch some subtly different cases; see below.
949+
for i in 0..1 {
950+
let mut db = ExecuteValidateLoggerDatabase::default();
951+
let a_in = Inputs::new(&db, vec![]);
952+
let b_in = Inputs::new(&db, vec![]);
953+
let c_in = Inputs::new(&db, vec![]);
954+
let d_in = Inputs::new(&db, vec![]);
955+
let e_in = Inputs::new(&db, vec![]);
956+
let a = Input::MinPanic(a_in);
957+
let b = Input::MinIterate(b_in);
958+
let c = Input::MinPanic(c_in);
959+
let d = Input::MinIterate(d_in);
960+
let e = Input::MinIterate(e_in);
961+
a_in.set_inputs(&mut db).to(vec![value(59), b.clone()]);
962+
b_in.set_inputs(&mut db).to(vec![value(60), c.clone()]);
963+
c_in.set_inputs(&mut db).to(vec![d.clone(), e.clone()]);
964+
d_in.set_inputs(&mut db)
965+
.to(vec![value(61), b.clone(), e.clone()]);
966+
e_in.set_inputs(&mut db).to(vec![d.clone()]);
967+
968+
a.clone().assert_value(&db, 59);
969+
b.clone().assert_value(&db, 60);
970+
971+
// First time we run this test, don't fetch c/d/e here; this means they won't get marked
972+
// `verified_final` in R6 (this revision), which will leave us in the next revision (R7)
973+
// with a chain of could-be-provisional memos from the previous revision which should be
974+
// final but were never confirmed as such; this triggers the case in `deep_verify_memo`
975+
// where we need to double-check `validate_provisional` after traversing dependencies.
976+
//
977+
// Second time we run this test, fetch everything in R6, to check the behavior of
978+
// `maybe_changed_after` with all validated-final memos.
979+
if i == 1 {
980+
c.clone().assert_value(&db, 60);
981+
d.clone().assert_value(&db, 60);
982+
e.clone().assert_value(&db, 60);
983+
}
984+
985+
db.assert_logs_len(27 + i);
986+
987+
// next revision, we change only A, which is not part of the cycle and the cycle does not
988+
// depend on.
989+
a_in.set_inputs(&mut db).to(vec![value(45), b.clone()]);
990+
b.assert_value(&db, 60);
991+
992+
db.assert_logs(expect![[r#"
993+
[
994+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(1)) })",
995+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(3)) })",
996+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(4)) })",
997+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(3)) })",
998+
"salsa_event(DidValidateMemoizedValue { database_key: min_panic(Id(2)) })",
999+
"salsa_event(DidValidateMemoizedValue { database_key: min_iterate(Id(1)) })",
1000+
]"#]]);
1001+
1002+
a.assert_value(&db, 45);
1003+
}
1004+
}

0 commit comments

Comments
 (0)