Skip to content

Commit b2b82bc

Browse files
authored
fix: change detection for fixpoint queries (#836)
* bug: Fix missing cycle inputs * Pass provisional as old memo to `execute_query` during fixpoint * Remove `provisional` from remove stale output * Revert debug code * Update test * Some documentation * Clean up tests * Fix for direct enclosing query * Format * More comment fiddling * Revert copy only outputs change * Revert `fixpoint_initial` start revision change (worth its own PR) * Align fixpoint handling with Derived * Preserve cycle heads when returning fixpoint initial * Always return changed for `Fixpoint::Initial` * Keep returning unchanged in some cases
1 parent 2c04176 commit b2b82bc

20 files changed

+399
-108
lines changed

src/accumulator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ impl<A: Accumulator> Ingredient for IngredientImpl<A> {
110110
_db: &dyn Database,
111111
_input: Id,
112112
_revision: Revision,
113+
_in_cycle: bool,
113114
) -> VerifyResult {
114115
panic!("nothing should ever depend on an accumulator directly")
115116
}

src/active_query.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ pub(crate) struct ActiveQuery {
6666
}
6767

6868
impl ActiveQuery {
69+
pub(super) fn seed_iteration(
70+
&mut self,
71+
durability: Durability,
72+
changed_at: Revision,
73+
edges: &[QueryEdge],
74+
untracked_read: bool,
75+
) {
76+
assert!(self.input_outputs.is_empty());
77+
self.input_outputs = edges.iter().cloned().collect();
78+
self.durability = self.durability.min(durability);
79+
self.changed_at = self.changed_at.max(changed_at);
80+
self.untracked_read |= untracked_read;
81+
}
82+
6983
pub(super) fn add_read(
7084
&mut self,
7185
input: DatabaseKeyIndex,

src/function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,11 @@ where
232232
db: &dyn Database,
233233
input: Id,
234234
revision: Revision,
235+
in_cycle: bool,
235236
) -> VerifyResult {
236237
// SAFETY: The `db` belongs to the ingredient as per caller invariant
237238
let db = unsafe { self.view_caster.downcast_unchecked(db) };
238-
self.maybe_changed_after(db, input, revision)
239+
self.maybe_changed_after(db, input, revision, in_cycle)
239240
}
240241

241242
/// True if the input `input` contains a memo that cites itself as a cycle head.
@@ -285,7 +286,6 @@ where
285286
_db: &dyn Database,
286287
_executor: DatabaseKeyIndex,
287288
_stale_output_key: crate::Id,
288-
_provisional: bool,
289289
) {
290290
// This function is invoked when a query Q specifies the value for `stale_output_key` in rev 1,
291291
// but not in rev 2. We don't do anything in this case, we just leave the (now stale) memo.

src/function/backdate.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::function::memo::Memo;
22
use crate::function::{Configuration, IngredientImpl};
33
use crate::zalsa_local::QueryRevisions;
4+
use crate::DatabaseKeyIndex;
45

56
impl<C> IngredientImpl<C>
67
where
@@ -12,6 +13,7 @@ where
1213
pub(super) fn backdate_if_appropriate<'db>(
1314
&self,
1415
old_memo: &Memo<C::Output<'db>>,
16+
index: DatabaseKeyIndex,
1517
revisions: &mut QueryRevisions,
1618
value: &C::Output<'db>,
1719
) {
@@ -24,7 +26,7 @@ where
2426
&& C::values_equal(old_value, value)
2527
{
2628
tracing::debug!(
27-
"value is equal, back-dating to {:?}",
29+
"{index:?} value is equal, back-dating to {:?}",
2830
old_memo.revisions.changed_at,
2931
);
3032

src/function/diff_outputs.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ where
2424
key: DatabaseKeyIndex,
2525
old_memo: &Memo<C::Output<'_>>,
2626
revisions: &mut QueryRevisions,
27-
provisional: bool,
2827
) {
2928
// Iterate over the outputs of the `old_memo` and put them into a hashset
3029
let mut old_outputs: FxIndexSet<_> = old_memo.revisions.origin.outputs().collect();
@@ -50,7 +49,7 @@ where
5049
});
5150

5251
for old_output in old_outputs {
53-
Self::report_stale_output(zalsa, db, key, old_output, provisional);
52+
Self::report_stale_output(zalsa, db, key, old_output);
5453
}
5554
}
5655

@@ -59,14 +58,13 @@ where
5958
db: &C::DbView,
6059
key: DatabaseKeyIndex,
6160
output: DatabaseKeyIndex,
62-
provisional: bool,
6361
) {
6462
db.salsa_event(&|| {
6563
Event::new(EventKind::WillDiscardStaleOutput {
6664
execute_key: key,
6765
output_key: output,
6866
})
6967
});
70-
output.remove_stale_output(zalsa, db.as_dyn_database(), key, provisional);
68+
output.remove_stale_output(zalsa, db.as_dyn_database(), key);
7169
}
7270
}

src/function/execute.rs

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,11 @@ where
101101
// really change, even if some of its inputs have. So we can
102102
// "backdate" its `changed_at` revision to be the same as the
103103
// old value.
104-
self.backdate_if_appropriate(old_memo, &mut revisions, &new_value);
104+
self.backdate_if_appropriate(old_memo, database_key_index, &mut revisions, &new_value);
105105

106106
// Diff the new outputs with the old, to discard any no-longer-emitted
107107
// outputs and update the tracked struct IDs for seeding the next revision.
108-
let provisional = !revisions.cycle_heads.is_empty();
109-
self.diff_outputs(
110-
zalsa,
111-
db,
112-
database_key_index,
113-
old_memo,
114-
&mut revisions,
115-
provisional,
116-
);
108+
self.diff_outputs(zalsa, db, database_key_index, old_memo, &mut revisions);
117109
}
118110
self.insert_memo(
119111
zalsa,
@@ -142,8 +134,14 @@ where
142134
// only when a cycle is actually encountered.
143135
let mut opt_last_provisional: Option<&Memo<<C as Configuration>::Output<'db>>> = None;
144136
loop {
145-
let (mut new_value, mut revisions) =
146-
Self::execute_query(db, active_query, opt_old_memo, zalsa.current_revision(), id);
137+
let previous_memo = opt_last_provisional.or(opt_old_memo);
138+
let (mut new_value, mut revisions) = Self::execute_query(
139+
db,
140+
active_query,
141+
previous_memo,
142+
zalsa.current_revision(),
143+
id,
144+
);
147145

148146
// Did the new result we got depend on our own provisional value, in a cycle?
149147
if revisions.cycle_heads.contains(&database_key_index) {
@@ -255,27 +253,25 @@ where
255253
current_revision: Revision,
256254
id: Id,
257255
) -> (C::Output<'db>, QueryRevisions) {
258-
// If we already executed this query once, then use the tracked-struct ids from the
259-
// previous execution as the starting point for the new one.
260256
if let Some(old_memo) = opt_old_memo {
257+
// If we already executed this query once, then use the tracked-struct ids from the
258+
// previous execution as the starting point for the new one.
261259
active_query.seed_tracked_struct_ids(&old_memo.revisions.tracked_struct_ids);
260+
261+
// Copy over all inputs and outputs from a previous iteration.
262+
// This is necessary to:
263+
// * ensure that tracked struct created during the previous iteration
264+
// (and are owned by the query) are alive even if the query in this iteration no longer creates them.
265+
// * ensure the final returned memo depends on all inputs from all iterations.
266+
if old_memo.may_be_provisional() && old_memo.verified_at.load() == current_revision {
267+
active_query.seed_iteration(&old_memo.revisions);
268+
}
262269
}
263270

264271
// Query was not previously executed, or value is potentially
265272
// stale, or value is absent. Let's execute!
266273
let new_value = C::execute(db, C::id_to_input(db, id));
267274

268-
if let Some(old_memo) = opt_old_memo {
269-
// Copy over all outputs from a previous iteration.
270-
// This is necessary to ensure that tracked struct created during the previous iteration
271-
// (and are owned by the query) are alive even if the query in this iteration no longer creates them.
272-
// The query not re-creating the tracked struct doesn't guarantee that there
273-
// aren't any other queries depending on it.
274-
if old_memo.may_be_provisional() && old_memo.verified_at.load() == current_revision {
275-
active_query.append_outputs(old_memo.revisions.origin.outputs());
276-
}
277-
}
278-
279275
(new_value, active_query.pop())
280276
}
281277
}

src/function/fetch.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,10 @@ where
138138
"hit cycle at {database_key_index:#?}, \
139139
inserting and returning fixpoint initial value"
140140
);
141-
let revisions = QueryRevisions::fixpoint_initial(
142-
database_key_index,
143-
zalsa.current_revision(),
144-
);
145-
let initial_value = self.initial_value(db, id).expect(
146-
"`CycleRecoveryStrategy::Fixpoint` \
147-
should have initial_value",
148-
);
141+
let revisions = QueryRevisions::fixpoint_initial(database_key_index);
142+
let initial_value = self
143+
.initial_value(db, id)
144+
.expect("`CycleRecoveryStrategy::Fixpoint` should have initial_value");
149145
Some(self.insert_memo(
150146
zalsa,
151147
id,
@@ -159,8 +155,7 @@ where
159155
);
160156
let active_query = db.zalsa_local().push_query(database_key_index, 0);
161157
let fallback_value = self.initial_value(db, id).expect(
162-
"`CycleRecoveryStrategy::FallbackImmediate` \
163-
should have initial_value",
158+
"`CycleRecoveryStrategy::FallbackImmediate` should have initial_value",
164159
);
165160
let mut revisions = active_query.pop();
166161
revisions.cycle_heads = CycleHeads::initial(database_key_index);

0 commit comments

Comments
 (0)