Skip to main content

differential_dataflow/operators/
reduce.rs

1//! Applies a reduction function on records grouped by key.
2//!
3//! The `reduce` operator acts on `(key, val)` data.
4//! Records with the same key are grouped together, and a user-supplied reduction function is applied
5//! to the key and the list of values.
6//! The function is expected to populate a list of output values.
7//!
8//! The output can change at times that are joins of input times, not only at input times themselves,
9//! and the operator must determine at which times to re-evaluate the reduction. A machine-checked
10//! account of which times suffice lives in `formal/Differential/Coverage.lean`.
11
12use crate::Data;
13
14use std::marker::PhantomData;
15
16use timely::progress::frontier::Antichain;
17use timely::progress::Timestamp;
18use timely::dataflow::operators::Operator;
19use timely::dataflow::operators::CapabilitySet;
20use timely::dataflow::channels::pact::Pipeline;
21
22use crate::operators::arrange::{Arranged, TraceAgent};
23use crate::trace::{BatchCursor, BatchDiff, BatchKey, BatchReader, BatchVal, BatchValOwn, Builder, Cursor, Description, ExertionLogic, Navigable, Trace, TraceReader};
24use crate::trace::cursor::cursor_list;
25use crate::trace::implementations::containers::BatchContainer;
26
27/// Sort and deduplicate a list. Shared by the cursor and reference tactics (via their
28/// `use super::*`) and the proxy tactic (`crate::operators::reduce::sort_dedup`), which
29/// each previously carried an identical copy.
30#[inline(never)]
31pub(crate) fn sort_dedup<T: Ord>(list: &mut Vec<T>) {
32    list.dedup();
33    list.sort();
34    list.dedup();
35}
36
37/// A type that resolves a key-wise reduction over batches arriving on the input.
38///
39/// Unlike join, reduce does not suspend: its output is at most linear in its input, so a single
40/// `retire` runs the whole `[lower, upper)` interval to completion rather than yielding under a fuel
41/// budget.
42pub trait ReduceTactic<B1: BatchReader, B2: BatchReader<Time = B1::Time>> {
43    /// Retire the interval `[lower, upper)`, producing the output batches it informs.
44    ///
45    /// It is presented with the pre-existing input batches and output batches (those before `lower`),
46    /// the new input batches, and `held`: the times the operator currently holds capabilities for. It
47    /// reasons only about times, returning the output batches to ship — each tagged with the time at
48    /// which to ship it — and the new frontier of interesting times for the operator to hold.
49    ///
50    /// # Contract
51    ///
52    /// The driver ([`reduce_with_tactic`]) relies on the following; the first two are cheap to check
53    /// and are `debug_assert!`ed there.
54    ///
55    /// * **Ordered, tiling output.** The returned `(time, batch)` pairs are in ascending order and
56    ///   their descriptions *tile* `[lower, upper)`: the first batch's lower is `lower`, each batch's
57    ///   upper is the next batch's lower, and the last batch's upper is `upper` — no gaps, no overlaps.
58    ///   Sub-intervals with no updates are skipped; the next batch's lower simply picks up where the
59    ///   last left off. Producing *in order* is a requirement, not a convenience — it is what lets the
60    ///   driver check the tiling with a single linear scan.
61    /// * **Shipped at a held time.** Each batch's `time` tag is an element of `held`; the driver mints
62    ///   a capability at it, which is only valid for a held time.
63    /// * **Frontier bounds withheld work, and collapses to empty when there is none.** The returned
64    ///   frontier must be at-or-below every time the tactic defers, so the driver knows what is safe to
65    ///   release. In particular, with no work to defer it must be the *empty* antichain. Derive it from
66    ///   the actual withheld set rather than constructing it and this holds for free; returning a
67    ///   non-empty frontier with nothing pending holds capabilities forever and **deadlocks recursive
68    ///   scopes**. (Not driver-checkable — the withheld set is tactic-internal — so tactics self-enforce.)
69    fn retire(
70        &mut self,
71        source_batches: Vec<B1>,
72        output_batches: Vec<B2>,
73        input_batches: Vec<B1>,
74        lower: &Antichain<B1::Time>,
75        upper: &Antichain<B1::Time>,
76        held: &Antichain<B1::Time>,
77    ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>);
78}
79
80/// A key-wise reduction of values in an input trace.
81///
82/// This method exists to provide reduce functionality without opinions about qualifying trace types.
83///
84/// The `logic` closure is expected to take a key, accumulated input, and tentative accumulated output,
85/// and populate its final argument with whatever it feels to be appopriate updates. The behavior and
86/// correctness of the implementation rely on this making sense, and e.g. ideally the updates would if
87/// applied to the tentative output bring it in line with some function applied to the input.
88///
89/// The `push` closure is expected to clear its first argument, then populate it with the key and drain
90/// the value updates, as appropriate for the container. It is critical that it clear the container as
91/// the operator has no ability to do this otherwise, and failing to do so represents a leak from one
92/// key's computation to another, and will likely introduce non-determinism.
93pub fn reduce_trace<'scope, Tr1, Bu, Tr2, KC, L, P>(trace: Arranged<'scope, Tr1>, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
94where
95    Tr1: TraceReader<Batch: Navigable> + 'static,
96    Tr2: Trace<Batch: Navigable, Time = Tr1::Time> + 'static,
97    KC: BatchContainer,
98    BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
99    for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
100    for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, ValOwn: Data, Time = Tr2::Time>,
101    Bu: Builder<Time=Tr2::Time, Output = Tr2::Batch, Input: Default> + 'static,
102    L: FnMut(KC::ReadItem<'_>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
103    P: FnMut(&mut Bu::Input, KC::ReadItem<'_>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
104{
105    reduce_with_tactic(trace, name, cursors::CursorTactic::<Tr1::Batch, Tr2::Batch, Bu, L, P>::new(logic, push))
106}
107
108// The model-derived reference tactic and its entry point live in `mod reference`; re-exported here
109// (doc-hidden) as the sole public handle for its differential and oracle tests.
110#[doc(hidden)]
111pub use reference::reduce_trace_reference;
112
113/// Drives a key-wise reduction using a supplied [`ReduceTactic`].
114///
115/// This is the general reduce operator: it does the dataflow plumbing (frontiers, capabilities, output
116/// trace maintenance) and routes the per-interval work through the tactic. It requires only
117/// `TraceReader` of its input and `Trace` of its output, never `Navigable`: it extracts batches via
118/// `batches_through`, and building cursors over them (if that is how the reduce proceeds) is the
119/// tactic's concern.
120pub fn reduce_with_tactic<'scope, Tr1, Tr2, T>(trace: Arranged<'scope, Tr1>, name: &str, mut tactic: T) -> Arranged<'scope, TraceAgent<Tr2>>
121where
122    Tr1: TraceReader + 'static,
123    Tr2: Trace<Time = Tr1::Time> + 'static,
124    T: ReduceTactic<Tr1::Batch, Tr2::Batch> + 'static,
125{
126    let mut result_trace = None;
127
128    // fabricate a data-parallel operator using the `unary_notify` pattern.
129    let stream = {
130
131        let mut source_trace = trace.trace;
132        let result_trace = &mut result_trace;
133        let scope = trace.stream.scope();
134        trace.stream.unary_frontier(Pipeline, name, move |_capability, operator_info| {
135
136            // Acquire a logger for arrange events.
137            let logger = scope.worker().logger_for::<crate::logging::DifferentialEventBuilder>("differential/arrange").map(Into::into);
138
139            let activator = Some(scope.activator_for(std::rc::Rc::clone(&operator_info.address)));
140            let mut empty = Tr2::new(operator_info.clone(), logger.clone(), activator);
141            // If there is default exert logic set, install it.
142            if let Some(exert_logic) = scope.worker().config().get::<ExertionLogic>("differential/default_exert_logic").cloned() {
143                empty.set_exert_logic(exert_logic);
144            }
145
146            let (mut output_reader, mut output_writer) = TraceAgent::new(empty, operator_info, logger);
147
148            *result_trace = Some(output_reader.clone());
149
150            // Capabilities for the lower envelope of the interesting times the operator holds.
151            let mut capabilities = CapabilitySet::<Tr1::Time>::default();
152
153            // Upper and lower frontiers for the pending input and output batches to process.
154            let mut upper_limit = Antichain::from_elem(<Tr1::Time as Timestamp>::minimum());
155            let mut lower_limit = Antichain::from_elem(<Tr1::Time as Timestamp>::minimum());
156
157            move |(input, frontier), output| {
158
159                // The operator receives input batches, which it treats as contiguous and will collect and
160                // then process as one batch. It captures the input frontier from the batches, from the upstream
161                // trace, and from the input frontier, and retires the work through that interval.
162                //
163                // Reduce may retain capabilities and need to perform work and produce output at times that
164                // may not be seen in its input. The standard example is that updates at `(0, 1)` and `(1, 0)`
165                // may result in outputs at `(1, 1)` as well, even with no input at that time.
166
167                let mut batch_storage = Vec::new();
168
169                // Downgrade previous upper limit to be current lower limit.
170                lower_limit.clear();
171                lower_limit.extend(upper_limit.borrow().iter().cloned());
172
173                // Drain input batches in order, capturing capabilities and the last upper.
174                input.for_each(|capability, batches| {
175                    capabilities.insert(capability.retain(0));
176                    for batch in batches.drain(..) {
177                        upper_limit.clone_from(batch.upper());
178                        batch_storage.push(batch);
179                    }
180                });
181
182                // Pull in any subsequent empty batches we believe to exist.
183                source_trace.advance_upper(&mut upper_limit);
184                // Incorporate the input frontier guarantees as well.
185                let mut joined = Antichain::new();
186                crate::lattice::antichain_join_into(&upper_limit.borrow()[..], &frontier.frontier()[..], &mut joined);
187                upper_limit = joined;
188
189                // We plan to retire the interval [lower_limit, upper_limit), which should be non-empty to proceed.
190                if upper_limit != lower_limit {
191
192                    // Acquire the pre-existing input and output batches preceding the interval. Batch handles
193                    // are cheap to clone, so we fetch them whether or not the tactic finds work to do.
194                    let source_batches = source_trace.batches_through(lower_limit.borrow()).expect("failed to acquire source batches");
195                    let output_batches = output_reader.batches_through(lower_limit.borrow()).expect("failed to acquire output batches");
196
197                    // The times the operator currently holds capabilities for, as an antichain.
198                    let held: Antichain<Tr1::Time> = capabilities.iter().map(|c| c.time().clone()).collect();
199
200                    // Retire the interval. The tactic reasons only about times: it returns output batches
201                    // each tagged with the time to ship it at, and the new frontier of interesting times.
202                    let (produced, new_frontier) = tactic.retire(source_batches, output_batches, batch_storage, &lower_limit, &upper_limit, &held);
203
204                    // Contract checks (see `ReduceTactic::retire`). Cheap, debug-only.
205                    debug_assert!(
206                        produced.iter().all(|(time, _)| held.elements().contains(time)),
207                        "ReduceTactic::retire shipped a batch at a time not held as a capability",
208                    );
209                    debug_assert!(
210                        {
211                            // Ordered output makes tiling a single linear scan: each description's lower
212                            // must meet the previous upper (starting at `lower_limit`), ending at `upper_limit`.
213                            let mut edge = lower_limit.clone();
214                            let abutting = produced.iter().all(|(_, batch)| {
215                                let matches = batch.description().lower() == &edge;
216                                edge.clone_from(batch.description().upper());
217                                matches
218                            });
219                            abutting && (produced.is_empty() || edge == upper_limit)
220                        },
221                        "ReduceTactic::retire output must be ordered and tile [lower, upper)",
222                    );
223
224                    // Ship each batch at a capability minted from the set at its time, and commit it to the
225                    // output trace. The times are elements of `held`, so they stay valid until we downgrade.
226                    for (time, batch) in produced {
227                        let capability = capabilities.delayed(&time);
228                        output.session(&capability).give(batch.clone());
229                        output_writer.insert(batch, Some(time));
230                    }
231
232                    // Downgrade to the frontier the tactic handed back (a no-op when it found no work).
233                    capabilities.downgrade(new_frontier);
234
235                    // ensure that observed progress is reflected in the output.
236                    output_writer.seal(upper_limit.clone());
237
238                    // We only anticipate future times in advance of `upper_limit`.
239                    source_trace.set_logical_compaction(upper_limit.borrow());
240                    output_reader.set_logical_compaction(upper_limit.borrow());
241
242                    // We will only slice the data between future batches.
243                    source_trace.set_physical_compaction(upper_limit.borrow());
244                    output_reader.set_physical_compaction(upper_limit.borrow());
245                }
246
247                // Exert trace maintenance if we have been so requested.
248                output_writer.exert();
249            }
250        }
251    )
252    };
253
254    Arranged { stream, trace: result_trace.unwrap() }
255}
256
257/// The conventional cursor-based [`ReduceTactic`].
258///
259/// It builds a [`CursorList`](crate::trace::cursor::CursorList) over the input, output, and new-batch
260/// updates and replays them together per key, applying `logic` and shaping output with `push`. It holds
261/// the outstanding synthetic interesting `(key, time)` moments across activations, and reasons only
262/// about times: capabilities are the driver's concern.
263mod cursors {
264
265    use super::*;
266
267    /// The conventional cursor-based [`ReduceTactic`].
268    pub struct CursorTactic<B1, B2, Bu, L, P>
269    where
270        B1: BatchReader + Navigable,
271        B2: BatchReader<Time = B1::Time> + Navigable,
272        B1::Cursor: Cursor<Time = B1::Time>,
273        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
274    {
275        logic: L,
276        push: P,
277        // Outstanding `(key, time)` synthetic interesting moments, sorted by `(key, time)`, and the
278        // buffers into which we assemble the next round's moments.
279        pending_keys: <B1::Cursor as Cursor>::KeyContainer,
280        pending_time: <B1::Cursor as Cursor>::TimeContainer,
281        next_pending_keys: <B1::Cursor as Cursor>::KeyContainer,
282        next_pending_time: <B1::Cursor as Cursor>::TimeContainer,
283        // Buffers reused across activations.
284        interesting_times: Vec<B1::Time>,
285        new_interesting_times: Vec<B1::Time>,
286        // Output batches may need to be built piecemeal, and these temp storage help there.
287        output_upper: Antichain<B1::Time>,
288        output_lower: Antichain<B1::Time>,
289        _marker: PhantomData<(B2, Bu)>,
290    }
291
292    impl<B1, B2, Bu, L, P> CursorTactic<B1, B2, Bu, L, P>
293    where
294        B1: BatchReader + Navigable,
295        B2: BatchReader<Time = B1::Time> + Navigable,
296        B1::Cursor: Cursor<Time = B1::Time>,
297        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
298    {
299        /// Construct a tactic that applies `logic` to each key and shapes output with `push`.
300        pub fn new(logic: L, push: P) -> Self {
301            CursorTactic {
302                logic,
303                push,
304                pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
305                pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
306                next_pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
307                next_pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
308                interesting_times: Vec::new(),
309                new_interesting_times: Vec::new(),
310                output_upper: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
311                output_lower: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
312                _marker: PhantomData,
313            }
314        }
315    }
316
317    impl<B1, B2, Bu, L, P> ReduceTactic<B1, B2> for CursorTactic<B1, B2, Bu, L, P>
318    where
319        B1: BatchReader + Navigable,
320        B2: BatchReader<Time = B1::Time> + Navigable,
321        B1::Cursor: Cursor<Time = B1::Time>,
322        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
323        Bu: Builder<Time = B1::Time, Output = B2, Input: Default>,
324        L: FnMut(<B1::Cursor as Cursor>::Key<'_>, &[(<B1::Cursor as Cursor>::Val<'_>, <B1::Cursor as Cursor>::Diff)], &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>),
325        P: FnMut(&mut Bu::Input, <B1::Cursor as Cursor>::Key<'_>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>),
326    {
327        fn retire(
328            &mut self,
329            source_batches: Vec<B1>,
330            output_batches: Vec<B2>,
331            input_batches: Vec<B1>,
332            lower: &Antichain<B1::Time>,
333            upper: &Antichain<B1::Time>,
334            held: &Antichain<B1::Time>,
335        ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>)
336        {
337            let mut produced = Vec::new();
338
339            // We have compute needs only if we hold a time in the interval [lower, upper); otherwise we
340            // could not transmit outputs even if they were (incorrectly) non-zero, and we leave the held
341            // times unchanged.
342            if held.elements().iter().any(|time| !upper.less_equal(time)) {
343
344                // cursors for navigating input, output, and new-batch updates.
345                let (mut source_cursor, ref source_storage) = cursor_list(source_batches);
346                let (mut output_cursor, ref output_storage) = cursor_list(output_batches);
347                let (mut batch_cursor, ref batch_storage) = cursor_list(input_batches);
348
349                // Prepare an output buffer and builder for each held time.
350                // TODO: It would be better if all updates went into one batch, but timely dataflow prevents
351                //       this as long as it requires that there is only one capability for each message.
352                let mut buffers = Vec::<(B1::Time, Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>)>::new();
353                let mut builders = Vec::new();
354                for time in held.elements().iter() {
355                    buffers.push((time.clone(), Vec::new()));
356                    builders.push(Bu::new());
357                }
358                // Temporary staging for output building.
359                let mut buffer = Bu::Input::default();
360
361                // Reuseable state for performing the computation.
362                let mut thinker = history_replay::HistoryReplayer::new();
363
364                // March through the keys we must work on, merging `batch_cursor` and pending keys.
365                // The interesting moments need to be in the interval to prompt work.
366                let mut pending_pos = 0;
367                while batch_cursor.key_valid(batch_storage) || pending_pos < self.pending_keys.len() {
368
369                    // Determine the next key we will work on; could be synthetic, could be from a batch.
370                    let key1 = self.pending_keys.get(pending_pos);
371                    let key2 = batch_cursor.get_key(batch_storage);
372                    let key = match (key1, key2) {
373                        (Some(key1), Some(key2)) => ::std::cmp::min(key1, key2),
374                        (Some(key1), None)       => key1,
375                        (None, Some(key2))       => key2,
376                        (None, None)             => unreachable!(),
377                    };
378
379                    // Populate `interesting_times` with interesting times not beyond `upper`.
380                    // TODO: This could just be `pending_time` and indexes within `lower .. upper`.
381                    let prior_pos = pending_pos;
382                    self.interesting_times.clear();
383                    while self.pending_keys.get(pending_pos) == Some(key) {
384                        let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pending_pos));
385                        if !upper.less_equal(&owned_time) { self.interesting_times.push(owned_time); }
386                        pending_pos += 1;
387                    }
388
389                    // tidy up times, removing redundancy.
390                    sort_dedup(&mut self.interesting_times);
391
392                    // If there are new updates, or pending times, we must investigate!
393                    if batch_cursor.get_key(batch_storage) == Some(key) || !self.interesting_times.is_empty() {
394
395                        // do the per-key computation.
396                        thinker.compute(
397                            key,
398                            (&mut source_cursor, source_storage),
399                            (&mut output_cursor, output_storage),
400                            (&mut batch_cursor, batch_storage),
401                            &self.interesting_times,
402                            &mut self.logic,
403                            upper,
404                            &mut buffers[..],
405                            &mut self.new_interesting_times,
406                        );
407
408                        // Advance the cursor if this key, so that the loop's validity check registers the work as done.
409                        if batch_cursor.get_key(batch_storage) == Some(key) { batch_cursor.step_key(batch_storage); }
410
411                        // Merge novel pending times with any prior pending times we did not process.
412                        // TODO: This could be a merge, not a sort_dedup, because both lists should be sorted.
413                        for pos in prior_pos .. pending_pos {
414                            let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pos));
415                            if upper.less_equal(&owned_time) { self.new_interesting_times.push(owned_time); }
416                        }
417                        sort_dedup(&mut self.new_interesting_times);
418                        for time in self.new_interesting_times.drain(..) {
419                            self.next_pending_keys.push_ref(key);
420                            self.next_pending_time.push_own(&time);
421                        }
422
423                        // Sort each buffer by value and move into the corresponding builder.
424                        // TODO: This makes assumptions about at least one of (i) the stability of `sort_by`,
425                        //       (ii) that the buffers are time-ordered, and (iii) that the builders accept
426                        //       arbitrarily ordered times.
427                        for index in 0 .. buffers.len() {
428                            buffers[index].1.sort_by(|x,y| x.0.cmp(&y.0));
429                            (self.push)(&mut buffer, key, &mut buffers[index].1);
430                            buffers[index].1.clear();
431                            builders[index].push(&mut buffer);
432
433                        }
434                    }
435                    else {
436                        // copy over the pending key and times.
437                        for pos in prior_pos .. pending_pos {
438                            self.next_pending_keys.push_ref(self.pending_keys.index(pos));
439                            self.next_pending_time.push_ref(self.pending_time.index(pos));
440                        }
441                    }
442                }
443                // Drop to avoid lifetime issues that would lock `pending_{keys, time}`.
444                drop(thinker);
445
446                // We start sealing output batches from the lower limit (previous upper limit).
447                // In principle, we could update `lower` itself, and it should arrive at `upper` by the
448                // end of the process.
449                self.output_lower.clear();
450                self.output_lower.extend(lower.borrow().iter().cloned());
451
452                // build each batch (because only one capability per message).
453                for (index, builder) in builders.drain(..).enumerate() {
454
455                    // Form the upper limit of the next batch, which includes all times greater
456                    // than the input batch, or the held times from i + 1 onward.
457                    self.output_upper.clear();
458                    self.output_upper.extend(upper.borrow().iter().cloned());
459                    for time in &held.elements()[index + 1 ..] {
460                        self.output_upper.insert_ref(time);
461                    }
462
463                    if self.output_upper.borrow() != self.output_lower.borrow() {
464
465                        let description = Description::new(self.output_lower.clone(), self.output_upper.clone(), Antichain::from_elem(<B1::Time as Timestamp>::minimum()));
466                        let batch = builder.done(description);
467
468                        // hand the batch back to the driver to ship and commit, tagged with its time.
469                        produced.push((held.elements()[index].clone(), batch));
470
471                        self.output_lower.clear();
472                        self.output_lower.extend(self.output_upper.borrow().iter().cloned());
473                    }
474                }
475                // This should be true, as the final iteration introduces no held times, and
476                // uses exactly `upper` to determine the upper bound. Good to check though.
477                assert!(self.output_upper.borrow() == upper.borrow());
478
479                // Refresh pending keys and times.
480                self.pending_keys.clear(); std::mem::swap(&mut self.next_pending_keys, &mut self.pending_keys);
481                self.pending_time.clear(); std::mem::swap(&mut self.next_pending_time, &mut self.pending_time);
482
483                // Compute the new frontier of interesting times for the operator to hold.
484                let mut frontier = Antichain::<B1::Time>::new();
485                let mut owned_time = <B1::Time as Timestamp>::minimum();
486                for pos in 0 .. self.pending_time.len() {
487                    <B1::Cursor as Cursor>::clone_time_onto(self.pending_time.index(pos), &mut owned_time);
488                    frontier.insert_ref(&owned_time);
489                }
490
491                (produced, frontier)
492            }
493            else {
494                // No work: leave the held times unchanged, so the driver's downgrade is a no-op.
495                (produced, held.clone())
496            }
497        }
498    }
499
500
501    /// Implementation based on replaying historical and new updates together.
502    mod history_replay {
503
504        use timely::progress::Antichain;
505
506        use crate::lattice::Lattice;
507        use crate::trace::Cursor;
508        use crate::operators::ValueHistory;
509
510        use crate::operators::reduce::sort_dedup;
511
512        /// The `HistoryReplayer` is a compute strategy based on moving through existing inputs, interesting times, etc in
513        /// time order, maintaining consolidated representations of updates with respect to future interesting times.
514        pub struct HistoryReplayer<V1, V2, V, T, D1, D2> {
515            input_history: ValueHistory<V1, T, D1>,
516            output_history: ValueHistory<V2, T, D2>,
517            batch_history: ValueHistory<V1, T, D1>,
518            input_buffer: Vec<(V1, D1)>,
519            output_buffer: Vec<(V, D2)>,
520            update_buffer: Vec<(V, D2)>,
521            output_produced: Vec<((V, T), D2)>,
522            synth_times: Vec<T>,
523            meets: Vec<T>,
524            times_current: Vec<T>,
525            temporary: Vec<T>,
526        }
527
528        impl<V1, V2, V, T, D1, D2> HistoryReplayer<V1, V2, V, T, D1, D2>
529        where
530            V1: Copy + Ord,
531            V2: Copy + Ord,
532            V: Clone + Ord,
533            T: Ord + Clone + Lattice,
534            D1: Clone + crate::difference::Semigroup,
535            D2: Clone + crate::difference::Semigroup,
536        {
537            pub fn new() -> Self {
538                HistoryReplayer {
539                    input_history: ValueHistory::new(),
540                    output_history: ValueHistory::new(),
541                    batch_history: ValueHistory::new(),
542                    input_buffer: Vec::new(),
543                    output_buffer: Vec::new(),
544                    update_buffer: Vec::new(),
545                    output_produced: Vec::new(),
546                    synth_times: Vec::new(),
547                    meets: Vec::new(),
548                    times_current: Vec::new(),
549                    temporary: Vec::new(),
550                }
551            }
552            #[inline(never)]
553            pub fn compute<'a, K, C1, C2, C3, L>(
554                &mut self,
555                key: K,
556                (source_cursor, source_storage): (&mut C1, &'a C1::Storage),
557                (output_cursor, output_storage): (&mut C2, &'a C2::Storage),
558                (batch_cursor, batch_storage): (&mut C3, &'a C3::Storage),
559                times: &Vec<T>,
560                logic: &mut L,
561                upper_limit: &Antichain<T>,
562                outputs: &mut [(T, Vec<(V, T, D2)>)],
563                new_interesting: &mut Vec<T>)
564            where
565                C1: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
566                C2: Cursor<Key<'a> = K, Val<'a> = V2, ValOwn = V, Time = T, Diff = D2>,
567                C3: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
568                K: Copy + Ord,
569                L: FnMut(K, &[(V1, D1)], &mut Vec<(V, D2)>, &mut Vec<(V, D2)>),
570            {
571
572                // The work we need to perform is at times defined principally by the contents of `batch_cursor`
573                // and `times`, respectively "new work we just received" and "old times we were warned about".
574                //
575                // Our first step is to identify these times, so that we can use them to restrict the amount of
576                // information we need to recover from `input` and `output`; as all times of interest will have
577                // some time from `batch_cursor` or `times`, we can compute their meet and advance all other
578                // loaded times by performing the lattice `join` with this value.
579
580                // Load the batch contents.
581                let mut batch_replay = self.batch_history.replay_key(batch_cursor, batch_storage, key, None);
582
583                // We determine the meet of times we must reconsider (those from `batch` and `times`). This meet
584                // can be used to advance other historical times, which may consolidate their representation. As
585                // a first step, we determine the meets of each *suffix* of `times`, which we will use as we play
586                // history forward.
587
588                self.meets.clear();
589                self.meets.extend(times.iter().cloned());
590                for index in (1 .. self.meets.len()).rev() {
591                    self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
592                }
593
594                // Determine the meet of times in `batch` and `times`.
595                let mut meet = None;
596                update_meet(&mut meet, self.meets.get(0));
597                update_meet(&mut meet, batch_replay.meet());
598
599                // Having determined the meet, we can load the input and output histories, where we
600                // advance all times by joining them with `meet`. The resulting times are more compact
601                // and guaranteed to accumulate identically for times greater or equal to `meet`.
602
603                // Load the input and output histories.
604                let mut input_replay =
605                self.input_history.replay_key(source_cursor, source_storage, key, meet.as_ref());
606                let mut output_replay =
607                self.output_history.replay_key(output_cursor, output_storage, key, meet.as_ref());
608
609                self.synth_times.clear();
610                self.times_current.clear();
611                self.output_produced.clear();
612
613                // The frontier of times we may still consider.
614                // Derived from frontiers of our update histories, supplied times, and synthetic times.
615
616                let mut times_slice = &times[..];
617                let mut meets_slice = &self.meets[..];
618
619                // We have candidate times from `batch` and `times`, as well as times identified by either
620                // `input` or `output`. Finally, we may have synthetic times produced as the join of times
621                // we consider in the course of evaluation. As long as any of these times exist, we need to
622                // keep examining times.
623                while let Some(next_time) = [   batch_replay.time(),
624                                                times_slice.first(),
625                                                input_replay.time(),
626                                                output_replay.time(),
627                                                self.synth_times.last(),
628                                            ].into_iter().flatten().min().cloned() {
629
630                    // Advance input and output history replayers. This marks applicable updates as active.
631                    input_replay.step_while_time_is(&next_time);
632                    output_replay.step_while_time_is(&next_time);
633
634                    // One of our goals is to determine if `next_time` is "interesting", meaning whether we
635                    // have any evidence that we should re-evaluate the user logic at this time. For a time
636                    // to be "interesting" it would need to be the join of times that include either a time
637                    // from `batch`, `times`, or `synth`. Neither `input` nor `output` times are sufficient.
638
639                    // Advance batch history, and capture whether an update exists at `next_time`.
640                    let mut interesting = batch_replay.step_while_time_is(&next_time);
641                    if interesting { if let Some(meet) = meet.as_ref() { batch_replay.advance_buffer_by(meet); } }
642
643                    // advance both `synth_times` and `times_slice`, marking this time interesting if in either.
644                    while self.synth_times.last() == Some(&next_time) {
645                        // We don't know enough about `next_time` to avoid putting it in to `times_current`.
646                        // TODO: If we knew that the time derived from a canceled batch update, we could remove the time.
647                        self.times_current.push(self.synth_times.pop().expect("failed to pop from synth_times")); // <-- TODO: this could be a min-heap.
648                        interesting = true;
649                    }
650                    while times_slice.first() == Some(&next_time) {
651                        // We know nothing about why we were warned about `next_time`, and must include it to scare future times.
652                        self.times_current.push(times_slice[0].clone());
653                        times_slice = &times_slice[1..];
654                        meets_slice = &meets_slice[1..];
655                        interesting = true;
656                    }
657
658                    // Times could also be interesting if an interesting time is less than them, as they would join
659                    // and become the time itself. They may not equal the current time because whatever frontier we
660                    // are tracking may not have advanced far enough.
661                    // TODO: `batch_history` may or may not be super compact at this point, and so this check might
662                    //       yield false positives if not sufficiently compact. Maybe we should look into this and see.
663                    interesting = interesting || batch_replay.buffer().iter().any(|&((_, ref t),_)| t.less_equal(&next_time));
664                    interesting = interesting || self.times_current.iter().any(|t| t.less_equal(&next_time));
665
666                    // We should only process times that are not in advance of `upper_limit`.
667                    //
668                    // We have no particular guarantee that known times will not be in advance of `upper_limit`.
669                    // We may have the guarantee that synthetic times will not be, as we test against the limit
670                    // before we add the time to `synth_times`.
671                    if !upper_limit.less_equal(&next_time) {
672
673                        // DETERMINATION (times only). Determine synthetic interesting times.
674                        //
675                        // Synthetic interesting times are produced differently for interesting and uninteresting
676                        // times. An uninteresting time must join with an interesting time to become interesting,
677                        // which means joins with `self.batch_history` and  `self.times_current`. I think we can
678                        // skip `self.synth_times` as we haven't gotten to them yet, but we will and they will be
679                        // joined against everything.
680
681                        // Any time, even uninteresting times, must be joined with the current accumulation of
682                        // batch times as well as the current accumulation of `times_current`.
683                        self.temporary.extend(batch_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
684                        self.temporary.extend(self.times_current.iter().filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
685
686                        // An interesting time additionally joins with `input` and `output` history and this round's
687                        // produced output: it carries the seed, so those joins stay interesting (an uninteresting
688                        // time does not, as `input`/`output` times are not themselves seeds). We advance the buffers
689                        // by `meet` first, exactly as evaluation reads them below; by join preservation the advanced
690                        // and unadvanced times spawn the same synthetics, so this matches the pre-split behavior.
691                        if interesting {
692                            if let Some(meet) = meet.as_ref() { input_replay.advance_buffer_by(meet) };
693                            if let Some(meet) = meet.as_ref() { output_replay.advance_buffer_by(meet) };
694                            self.temporary.extend(input_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
695                            self.temporary.extend(output_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
696                            self.temporary.extend(self.output_produced.iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
697                        }
698                        sort_dedup(&mut self.temporary);
699
700                        // Introduce synthetic times, and re-organize if we add any.
701                        let synth_len = self.synth_times.len();
702                        for time in self.temporary.drain(..) {
703                            // We can either service `join` now, or must delay for the future.
704                            if upper_limit.less_equal(&time) {
705                                debug_assert!(outputs.iter().any(|(t,_)| t.less_equal(&time)));
706                                new_interesting.push(time);
707                            }
708                            else {
709                                self.synth_times.push(time);
710                            }
711                        }
712                        if self.synth_times.len() > synth_len {
713                            self.synth_times.sort_by(|x,y| y.cmp(x));
714                            self.synth_times.dedup();
715                        }
716
717                        // EVALUATION (values only).
718                        // We should re-evaluate the computation if this is an interesting time.
719                        // If the time is uninteresting (and our logic is sound) it is not possible for there to be
720                        // output produced. This sounds like a good test to have for debug builds!
721                        if interesting {
722
723                            // Assemble the input collection at `next_time`. (`self.input_buffer` cleared just after use).
724                            // The buffers were advanced by `meet` in the determination step above.
725                            debug_assert!(self.input_buffer.is_empty());
726                            for ((value, time), diff) in input_replay.buffer().iter() {
727                                if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
728                            }
729                            for ((value, time), diff) in batch_replay.buffer().iter() {
730                                if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
731                            }
732                            crate::consolidation::consolidate(&mut self.input_buffer);
733
734                            // Assemble the output collection at `next_time`. (`self.output_buffer` cleared just after use).
735                            for ((value, time), diff) in output_replay.buffer().iter() {
736                                if time.less_equal(&next_time) { self.output_buffer.push((C2::owned_val(*value), diff.clone())); }
737                            }
738                            for ((value, time), diff) in self.output_produced.iter() {
739                                if time.less_equal(&next_time) { self.output_buffer.push(((*value).to_owned(), diff.clone())); }
740                            }
741                            crate::consolidation::consolidate(&mut self.output_buffer);
742
743                            // Apply user logic if non-empty input or output and see what happens!
744                            if !self.input_buffer.is_empty() || !self.output_buffer.is_empty() {
745                                logic(key, &self.input_buffer[..], &mut self.output_buffer, &mut self.update_buffer);
746                                self.input_buffer.clear();
747                                self.output_buffer.clear();
748
749                                // Having subtracted output updates from user output, consolidate the results to determine
750                                // if there is anything worth reporting. Note: this also orders the results by value, so
751                                // that could make the above merging plan even easier.
752                                //
753                                // Stash produced updates into both capability-indexed buffers and `output_produced`.
754                                // The two locations are important, in that we will compact `output_produced` as we move
755                                // through times, but we cannot compact the output buffers because we need their actual
756                                // times.
757                                crate::consolidation::consolidate(&mut self.update_buffer);
758                                if !self.update_buffer.is_empty() {
759
760                                    // We *should* be able to find a capability for `next_time`. Any thing else would
761                                    // indicate a logical error somewhere along the way; either we release a capability
762                                    // we should have kept, or we have computed the output incorrectly (or both!)
763                                    let idx = outputs.iter().rev().position(|(time, _)| time.less_equal(&next_time));
764                                    let idx = outputs.len() - idx.expect("failed to find index") - 1;
765                                    for (val, diff) in self.update_buffer.drain(..) {
766                                        self.output_produced.push(((val.clone(), next_time.clone()), diff.clone()));
767                                        outputs[idx].1.push((val, next_time.clone(), diff));
768                                    }
769
770                                    // Advance times in `self.output_produced` and consolidate the representation.
771                                    // NOTE: We only do this when we add records; it could be that there are situations
772                                    //       where we want to consolidate even without changes (because an initially
773                                    //       large collection can now be collapsed).
774                                    if let Some(meet) = meet.as_ref() { for entry in &mut self.output_produced { (entry.0).1.join_assign(meet); } }
775                                    crate::consolidation::consolidate(&mut self.output_produced);
776                                }
777                            }
778                        }
779                    }
780                    else if interesting {
781                        // We cannot process `next_time` now, and must delay it.
782                        //
783                        // I think we are probably only here because of an uninteresting time declared interesting,
784                        // as initial interesting times are filtered to be in interval, and synthetic times are also
785                        // filtered before introducing them to `self.synth_times`.
786                        new_interesting.push(next_time.clone());
787                        debug_assert!(outputs.iter().any(|(t,_)| t.less_equal(&next_time)))
788                    }
789
790                    // Update `meet` to track the meet of each source of times.
791                    meet = None;
792                    update_meet(&mut meet, batch_replay.meet());
793                    update_meet(&mut meet, input_replay.meet());
794                    update_meet(&mut meet, output_replay.meet());
795                    for time in self.synth_times.iter() { update_meet(&mut meet, Some(time)); }
796                    update_meet(&mut meet, meets_slice.first());
797
798                    // Update `times_current` by the frontier.
799                    if let Some(meet) = meet.as_ref() {
800                        for time in self.times_current.iter_mut() {
801                            *time = time.join(meet);
802                        }
803                    }
804
805                    sort_dedup(&mut self.times_current);
806                }
807
808                // Normalize the representation of `new_interesting`, deduplicating and ordering.
809                sort_dedup(new_interesting);
810            }
811        }
812
813        /// Updates an optional meet by an optional time.
814        fn update_meet<T: Lattice+Clone>(meet: &mut Option<T>, other: Option<&T>) {
815            if let Some(time) = other {
816                if let Some(meet) = meet.as_mut() { meet.meet_assign(time); }
817                else { *meet = Some(time.clone()); }
818            }
819        }
820    }
821}
822
823/// A second [`ReduceTactic`], written directly from the incremental model in
824/// `formal/Differential/Model.lean`.
825///
826/// Per key it runs two phases over one cursor walk. Phase 1 (determination) computes the
827/// interesting times as the truncated join-closure over {input, output, seeds} — the model's
828/// `Reached` — advancing by meets so the synthetic set stays bounded. Phase 2 (application) walks
829/// exactly those times in order, maintaining tight input/output accumulations by meets, and emits
830/// the corrections — the model's `emit_correct`. Determination never consults the output produced
831/// this round (it finishes first), so this tactic embodies the proven algorithm exactly and is the
832/// clean subject for differential testing against [`cursors::CursorTactic`].
833pub(crate) mod reference {
834
835    use super::*;
836    use crate::lattice::Lattice;
837    use crate::operators::ValueHistory;
838
839    /// Drives a key-wise reduction with the model-derived [`ReferenceTactic`], the analogue of the
840    /// default [`super::reduce_trace`]. Same result contract; intended for differential testing of the
841    /// two tactics against each other. Re-exported (doc-hidden) from the parent module as the sole
842    /// public handle: the reference tactic is a testing and demonstration oracle, not a stable entry
843    /// point to build on.
844    pub fn reduce_trace_reference<'scope, Tr1, Bu, Tr2, L, P>(trace: Arranged<'scope, Tr1>, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
845    where
846        Tr1: TraceReader<Batch: Navigable> + 'static,
847        Tr2: Trace<Batch: Navigable, Time = Tr1::Time> + 'static,
848        BatchCursor<Tr1>: Cursor<Time = Tr1::Time>,
849        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = BatchKey<'a, Tr1>, ValOwn: Data, Time = Tr2::Time>,
850        Bu: Builder<Time=Tr2::Time, Output = Tr2::Batch, Input: Default> + 'static,
851        L: FnMut(BatchKey<'_, Tr1>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
852        P: FnMut(&mut Bu::Input, BatchKey<'_, Tr1>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
853    {
854        reduce_with_tactic(trace, name, ReferenceTactic::<Tr1::Batch, Tr2::Batch, Bu, L, P>::new(logic, push))
855    }
856
857
858    /// Updates an optional meet by an optional time.
859    fn update_meet<T: Lattice+Clone>(meet: &mut Option<T>, other: Option<&T>) {
860        if let Some(time) = other {
861            if let Some(meet) = meet.as_mut() { meet.meet_assign(time); }
862            else { *meet = Some(time.clone()); }
863        }
864    }
865
866    /// The model-derived [`ReduceTactic`]. Structurally a twin of [`cursors::CursorTactic`]; only the
867    /// per-key engine differs.
868    pub struct ReferenceTactic<B1, B2, Bu, L, P>
869    where
870        B1: BatchReader + Navigable,
871        B2: BatchReader<Time = B1::Time> + Navigable,
872        B1::Cursor: Cursor<Time = B1::Time>,
873        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
874    {
875        logic: L,
876        push: P,
877        pending_keys: <B1::Cursor as Cursor>::KeyContainer,
878        pending_time: <B1::Cursor as Cursor>::TimeContainer,
879        next_pending_keys: <B1::Cursor as Cursor>::KeyContainer,
880        next_pending_time: <B1::Cursor as Cursor>::TimeContainer,
881        interesting_times: Vec<B1::Time>,
882        new_interesting_times: Vec<B1::Time>,
883        output_upper: Antichain<B1::Time>,
884        output_lower: Antichain<B1::Time>,
885        _marker: PhantomData<(B2, Bu)>,
886    }
887
888    impl<B1, B2, Bu, L, P> ReferenceTactic<B1, B2, Bu, L, P>
889    where
890        B1: BatchReader + Navigable,
891        B2: BatchReader<Time = B1::Time> + Navigable,
892        B1::Cursor: Cursor<Time = B1::Time>,
893        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
894    {
895        /// Construct a tactic that applies `logic` to each key and shapes output with `push`.
896        pub fn new(logic: L, push: P) -> Self {
897            ReferenceTactic {
898                logic,
899                push,
900                pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
901                pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
902                next_pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
903                next_pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
904                interesting_times: Vec::new(),
905                new_interesting_times: Vec::new(),
906                output_upper: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
907                output_lower: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
908                _marker: PhantomData,
909            }
910        }
911    }
912
913    impl<B1, B2, Bu, L, P> ReduceTactic<B1, B2> for ReferenceTactic<B1, B2, Bu, L, P>
914    where
915        B1: BatchReader + Navigable,
916        B2: BatchReader<Time = B1::Time> + Navigable,
917        B1::Cursor: Cursor<Time = B1::Time>,
918        for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
919        Bu: Builder<Time = B1::Time, Output = B2, Input: Default>,
920        L: FnMut(<B1::Cursor as Cursor>::Key<'_>, &[(<B1::Cursor as Cursor>::Val<'_>, <B1::Cursor as Cursor>::Diff)], &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>),
921        P: FnMut(&mut Bu::Input, <B1::Cursor as Cursor>::Key<'_>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>),
922    {
923        fn retire(
924            &mut self,
925            source_batches: Vec<B1>,
926            output_batches: Vec<B2>,
927            input_batches: Vec<B1>,
928            lower: &Antichain<B1::Time>,
929            upper: &Antichain<B1::Time>,
930            held: &Antichain<B1::Time>,
931        ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>)
932        {
933            let mut produced = Vec::new();
934
935            if held.elements().iter().any(|time| !upper.less_equal(time)) {
936
937                let (mut source_cursor, ref source_storage) = cursor_list(source_batches);
938                let (mut output_cursor, ref output_storage) = cursor_list(output_batches);
939                let (mut batch_cursor, ref batch_storage) = cursor_list(input_batches);
940
941                let mut buffers = Vec::<(B1::Time, Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>)>::new();
942                let mut builders = Vec::new();
943                for time in held.elements().iter() {
944                    buffers.push((time.clone(), Vec::new()));
945                    builders.push(Bu::new());
946                }
947                let mut buffer = Bu::Input::default();
948
949                // Reuseable state for performing the computation.
950                let mut thinker = ReferenceThinker::new();
951
952                let mut pending_pos = 0;
953                while batch_cursor.key_valid(batch_storage) || pending_pos < self.pending_keys.len() {
954
955                    let key1 = self.pending_keys.get(pending_pos);
956                    let key2 = batch_cursor.get_key(batch_storage);
957                    let key = match (key1, key2) {
958                        (Some(key1), Some(key2)) => ::std::cmp::min(key1, key2),
959                        (Some(key1), None)       => key1,
960                        (None, Some(key2))       => key2,
961                        (None, None)             => unreachable!(),
962                    };
963
964                    let prior_pos = pending_pos;
965                    self.interesting_times.clear();
966                    while self.pending_keys.get(pending_pos) == Some(key) {
967                        let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pending_pos));
968                        if !upper.less_equal(&owned_time) { self.interesting_times.push(owned_time); }
969                        pending_pos += 1;
970                    }
971
972                    sort_dedup(&mut self.interesting_times);
973
974                    if batch_cursor.get_key(batch_storage) == Some(key) || !self.interesting_times.is_empty() {
975
976                        thinker.compute(
977                            key,
978                            (&mut source_cursor, source_storage),
979                            (&mut output_cursor, output_storage),
980                            (&mut batch_cursor, batch_storage),
981                            &self.interesting_times,
982                            &mut self.logic,
983                            upper,
984                            &mut buffers[..],
985                            &mut self.new_interesting_times,
986                        );
987
988                        if batch_cursor.get_key(batch_storage) == Some(key) { batch_cursor.step_key(batch_storage); }
989
990                        for pos in prior_pos .. pending_pos {
991                            let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pos));
992                            if upper.less_equal(&owned_time) { self.new_interesting_times.push(owned_time); }
993                        }
994                        sort_dedup(&mut self.new_interesting_times);
995                        for time in self.new_interesting_times.drain(..) {
996                            self.next_pending_keys.push_ref(key);
997                            self.next_pending_time.push_own(&time);
998                        }
999
1000                        for index in 0 .. buffers.len() {
1001                            buffers[index].1.sort_by(|x,y| x.0.cmp(&y.0));
1002                            (self.push)(&mut buffer, key, &mut buffers[index].1);
1003                            buffers[index].1.clear();
1004                            builders[index].push(&mut buffer);
1005
1006                        }
1007                    }
1008                    else {
1009                        for pos in prior_pos .. pending_pos {
1010                            self.next_pending_keys.push_ref(self.pending_keys.index(pos));
1011                            self.next_pending_time.push_ref(self.pending_time.index(pos));
1012                        }
1013                    }
1014                }
1015                drop(thinker);
1016
1017                self.output_lower.clear();
1018                self.output_lower.extend(lower.borrow().iter().cloned());
1019
1020                for (index, builder) in builders.drain(..).enumerate() {
1021
1022                    self.output_upper.clear();
1023                    self.output_upper.extend(upper.borrow().iter().cloned());
1024                    for time in &held.elements()[index + 1 ..] {
1025                        self.output_upper.insert_ref(time);
1026                    }
1027
1028                    if self.output_upper.borrow() != self.output_lower.borrow() {
1029
1030                        let description = Description::new(self.output_lower.clone(), self.output_upper.clone(), Antichain::from_elem(<B1::Time as Timestamp>::minimum()));
1031                        let batch = builder.done(description);
1032
1033                        produced.push((held.elements()[index].clone(), batch));
1034
1035                        self.output_lower.clear();
1036                        self.output_lower.extend(self.output_upper.borrow().iter().cloned());
1037                    }
1038                }
1039                assert!(self.output_upper.borrow() == upper.borrow());
1040
1041                self.pending_keys.clear(); std::mem::swap(&mut self.next_pending_keys, &mut self.pending_keys);
1042                self.pending_time.clear(); std::mem::swap(&mut self.next_pending_time, &mut self.pending_time);
1043
1044                let mut frontier = Antichain::<B1::Time>::new();
1045                let mut owned_time = <B1::Time as Timestamp>::minimum();
1046                for pos in 0 .. self.pending_time.len() {
1047                    <B1::Cursor as Cursor>::clone_time_onto(self.pending_time.index(pos), &mut owned_time);
1048                    frontier.insert_ref(&owned_time);
1049                }
1050
1051                (produced, frontier)
1052            }
1053            else {
1054                (produced, held.clone())
1055            }
1056        }
1057    }
1058
1059    /// The two-phase per-key engine.
1060    ///
1061    /// Phase 1 (determination) reads the input/output/seed *times* and closes them into `active`
1062    /// (the interesting times) and the pended set — Model.lean's `Reached`, directly. Phase 2
1063    /// (application) walks the same, still-loaded histories for *values* and emits corrections.
1064    pub struct ReferenceThinker<V1, V2, V, T, D1, D2> {
1065        input_history: ValueHistory<V1, T, D1>,
1066        output_history: ValueHistory<V2, T, D2>,
1067        batch_history: ValueHistory<V1, T, D1>,
1068        input_buffer: Vec<(V1, D1)>,
1069        output_buffer: Vec<(V, D2)>,
1070        update_buffer: Vec<(V, D2)>,
1071        output_produced: Vec<((V, T), D2)>,
1072        // Phase 1 (the compacted closure): synthetic reached times still to visit, the reached times
1073        // in play as join partners, scratch for the joins, and suffix-meets of the supplied times.
1074        synth_times: Vec<T>,
1075        times_current: Vec<T>,
1076        temporary: Vec<T>,
1077        meets: Vec<T>,
1078        // Reusable time-only buffers for phase 1's `TimeReplay` walks. Pooled here (rather than in
1079        // `ValueHistory`) so the reference tactic pays for them and the standard value walk does not.
1080        batch_times: Vec<T>,
1081        input_times: Vec<T>,
1082        output_times: Vec<T>,
1083        // The interesting (in-band reached) times, handed from phase 1 to phase 2.
1084        active: Vec<T>,
1085    }
1086
1087    impl<V1, V2, V, T, D1, D2> ReferenceThinker<V1, V2, V, T, D1, D2>
1088    where
1089        V1: Copy + Ord,
1090        V2: Copy + Ord,
1091        V: Clone + Ord,
1092        T: Ord + Clone + Lattice + 'static,
1093        D1: Clone + crate::difference::Semigroup,
1094        D2: Clone + crate::difference::Semigroup,
1095    {
1096        pub fn new() -> Self {
1097            ReferenceThinker {
1098                input_history: ValueHistory::new(),
1099                output_history: ValueHistory::new(),
1100                batch_history: ValueHistory::new(),
1101                input_buffer: Vec::new(),
1102                output_buffer: Vec::new(),
1103                update_buffer: Vec::new(),
1104                output_produced: Vec::new(),
1105                synth_times: Vec::new(),
1106                times_current: Vec::new(),
1107                temporary: Vec::new(),
1108                meets: Vec::new(),
1109                batch_times: Vec::new(),
1110                input_times: Vec::new(),
1111                output_times: Vec::new(),
1112                active: Vec::new(),
1113            }
1114        }
1115
1116        #[inline(never)]
1117        pub fn compute<'a, K, C1, C2, C3, L>(
1118            &mut self,
1119            key: K,
1120            (source_cursor, source_storage): (&mut C1, &'a C1::Storage),
1121            (output_cursor, output_storage): (&mut C2, &'a C2::Storage),
1122            (batch_cursor, batch_storage): (&mut C3, &'a C3::Storage),
1123            times: &Vec<T>,
1124            logic: &mut L,
1125            upper_limit: &Antichain<T>,
1126            outputs: &mut [(T, Vec<(V, T, D2)>)],
1127            new_interesting: &mut Vec<T>)
1128        where
1129            C1: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
1130            C2: Cursor<Key<'a> = K, Val<'a> = V2, ValOwn = V, Time = T, Diff = D2>,
1131            C3: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
1132            K: Copy + Ord,
1133            L: FnMut(K, &[(V1, D1)], &mut Vec<(V, D2)>, &mut Vec<(V, D2)>),
1134        {
1135            // ================== PHASE 1 — DETERMINATION (`Reached`, compacted) ==================
1136            // The interesting times are Model.lean's `Reached` — but computed the non-quadratic way.
1137            // Walk the input/output/seed times in increasing order; a time is *reached* only via a
1138            // seed (a batch update, a due pending time, or a synthetic join of earlier reached times);
1139            // a reached in-band time joins against the live partners to spawn more reached times, and
1140            // a join beyond `upper` is pended. The live partner sets are kept an antichain by
1141            // `advance_buffer_by(meet)` — coincident times collapse under the running meet — so this is
1142            // the closure without the all-pairs blow-up. Time-only: `TimeReplay` reads the histories
1143            // without touching values or stepping the underlying `history`, so phase 2 can walk them.
1144            {
1145                // Suffix-meets of the supplied (due pending) times, consumed as we pass them.
1146                self.meets.clear();
1147                self.meets.extend(times.iter().cloned());
1148                for index in (1 .. self.meets.len()).rev() {
1149                    self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
1150                }
1151
1152                // Build each history, then read it time-only (leaving it intact for phase 2).
1153                drop(self.batch_history.replay_key(batch_cursor, batch_storage, key, None));
1154                drop(self.input_history.replay_key(source_cursor, source_storage, key, None));
1155                drop(self.output_history.replay_key(output_cursor, output_storage, key, None));
1156                let mut batch_replay = self.batch_history.replay_times(&mut self.batch_times);
1157                let mut input_replay = self.input_history.replay_times(&mut self.input_times);
1158                let mut output_replay = self.output_history.replay_times(&mut self.output_times);
1159
1160                self.synth_times.clear();
1161                self.times_current.clear();
1162                self.temporary.clear();
1163                self.active.clear();
1164
1165                let mut times_slice = &times[..];
1166                let mut meets_slice = &self.meets[..];
1167                let mut meet: Option<T> = None;
1168
1169                while let Some(next_time) = [   batch_replay.time(),
1170                                                times_slice.first(),
1171                                                input_replay.time(),
1172                                                output_replay.time(),
1173                                                self.synth_times.last(),
1174                                            ].into_iter().flatten().min().cloned() {
1175
1176                    input_replay.step_while_time_is(&next_time);
1177                    output_replay.step_while_time_is(&next_time);
1178
1179                    // Reached via a seed: a batch update, a due pending time, or a synthetic join.
1180                    // (Input/output times alone are not reached — they are only join partners.)
1181                    let mut interesting = batch_replay.step_while_time_is(&next_time);
1182                    if interesting { if let Some(m) = meet.as_ref() { batch_replay.advance_buffer_by(m); } }
1183                    while self.synth_times.last() == Some(&next_time) {
1184                        self.times_current.push(self.synth_times.pop().unwrap());
1185                        interesting = true;
1186                    }
1187                    while times_slice.first() == Some(&next_time) {
1188                        self.times_current.push(next_time.clone());
1189                        times_slice = &times_slice[1..];
1190                        meets_slice = &meets_slice[1..];
1191                        interesting = true;
1192                    }
1193                    // Absorb: a time at or above a reached time is itself reached.
1194                    interesting = interesting
1195                        || batch_replay.buffer().iter().any(|t| t.less_equal(&next_time))
1196                        || self.times_current.iter().any(|t| t.less_equal(&next_time));
1197
1198                    if !upper_limit.less_equal(&next_time) {
1199                        // A reached in-band time is `active`; join it against the live partners —
1200                        // input/output (`joinBase`) and reached-so-far (`joinActive`) — for new times.
1201                        if interesting {
1202                            self.active.push(next_time.clone());
1203                            if let Some(m) = meet.as_ref() { input_replay.advance_buffer_by(m); output_replay.advance_buffer_by(m); }
1204                            self.temporary.extend(input_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1205                            self.temporary.extend(output_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1206                        }
1207                        self.temporary.extend(batch_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1208                        self.temporary.extend(self.times_current.iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1209                        sort_dedup(&mut self.temporary);
1210
1211                        let synth_len = self.synth_times.len();
1212                        for time in self.temporary.drain(..) {
1213                            if upper_limit.less_equal(&time) { new_interesting.push(time); }  // pended
1214                            else { self.synth_times.push(time); }                             // reached, later
1215                        }
1216                        if self.synth_times.len() > synth_len {
1217                            self.synth_times.sort_by(|x,y| y.cmp(x));
1218                            self.synth_times.dedup();
1219                        }
1220                    }
1221                    else if interesting {
1222                        new_interesting.push(next_time.clone());
1223                    }
1224
1225                    // Running meet (a lower bound on every time still to visit); compact the reached
1226                    // partners `times_current` with it.
1227                    meet = None;
1228                    update_meet(&mut meet, batch_replay.meet());
1229                    update_meet(&mut meet, input_replay.meet());
1230                    update_meet(&mut meet, output_replay.meet());
1231                    for time in self.synth_times.iter() { update_meet(&mut meet, Some(time)); }
1232                    update_meet(&mut meet, meets_slice.first());
1233                    if let Some(m) = meet.as_ref() {
1234                        for time in self.times_current.iter_mut() { *time = time.join(m); }
1235                    }
1236                    sort_dedup(&mut self.times_current);
1237                }
1238
1239                sort_dedup(&mut self.active);
1240            }
1241
1242            // ===================== PHASE 2 — APPLICATION (`emit_correct`) =====================
1243            // Walk `self.active` in order over the SAME per-key edits (via `replay`, no cursor), keep
1244            // the input/output accumulations tight by advancing to the meet of the times still to be
1245            // produced, apply `logic`, and emit corrections.
1246            {
1247                self.meets.clear();
1248                self.meets.extend(self.active.iter().cloned());
1249                for index in (1 .. self.meets.len()).rev() {
1250                    self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
1251                }
1252
1253                // Walk the histories loaded (and left intact) by phase 1 — no cursor re-read, no
1254                // rebuild, no re-sort; just a fresh walk of the same sorted `history` for values.
1255                let mut batch_replay = self.batch_history.walk();
1256                let mut input_replay = self.input_history.walk();
1257                let mut output_replay = self.output_history.walk();
1258
1259                self.output_produced.clear();
1260
1261                for index in 0 .. self.active.len() {
1262                    let next_time = self.active[index].clone();
1263                    let meet = self.meets[index].clone();
1264
1265                    // Phase 2 visits only the active times, so at each we must catch up the histories to
1266                    // include every edit that will contribute to the accumulation at `next_time` (edits
1267                    // at non-active times count too). `history` is sorted by the total `Ord` and `step`
1268                    // pops the least, so we step the `Ord`-prefix `t <= next_time`, NOT the partial
1269                    // `t.less_equal(next_time)`: the partial order interleaves with the sort, so an
1270                    // `Ord`-earlier time incomparable to `next_time` would halt a `less_equal` walk early
1271                    // and strand later edits that *are* `less_equal(next_time)`. Stepping the `Ord`-prefix
1272                    // takes a superset; the `less_equal` filter below then selects the true `<= next_time`
1273                    // edits. (Assembling with `less_equal` while stepping with `<=` is what the value-aware
1274                    // cursor does via its full time-order frontier walk.)
1275                    while input_replay.time().map_or(false, |t| *t <= next_time) { input_replay.step(); }
1276                    while batch_replay.time().map_or(false, |t| *t <= next_time) { batch_replay.step(); }
1277                    while output_replay.time().map_or(false, |t| *t <= next_time) { output_replay.step(); }
1278                    input_replay.advance_buffer_by(&meet);
1279                    batch_replay.advance_buffer_by(&meet);
1280                    output_replay.advance_buffer_by(&meet);
1281
1282                    debug_assert!(self.input_buffer.is_empty());
1283                    for ((value, time), diff) in input_replay.buffer().iter() {
1284                        if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
1285                    }
1286                    for ((value, time), diff) in batch_replay.buffer().iter() {
1287                        if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
1288                    }
1289                    crate::consolidation::consolidate(&mut self.input_buffer);
1290
1291                    for ((value, time), diff) in output_replay.buffer().iter() {
1292                        if time.less_equal(&next_time) { self.output_buffer.push((C2::owned_val(*value), diff.clone())); }
1293                    }
1294                    for ((value, time), diff) in self.output_produced.iter() {
1295                        if time.less_equal(&next_time) { self.output_buffer.push((value.clone(), diff.clone())); }
1296                    }
1297                    crate::consolidation::consolidate(&mut self.output_buffer);
1298
1299                    if !self.input_buffer.is_empty() || !self.output_buffer.is_empty() {
1300                        logic(key, &self.input_buffer[..], &mut self.output_buffer, &mut self.update_buffer);
1301                        self.input_buffer.clear();
1302                        self.output_buffer.clear();
1303
1304                        crate::consolidation::consolidate(&mut self.update_buffer);
1305                        if !self.update_buffer.is_empty() {
1306
1307                            let idx = outputs.iter().rev().position(|(time, _)| time.less_equal(&next_time));
1308                            let idx = outputs.len() - idx.expect("failed to find index") - 1;
1309                            for (val, diff) in self.update_buffer.drain(..) {
1310                                self.output_produced.push(((val.clone(), next_time.clone()), diff.clone()));
1311                                outputs[idx].1.push((val, next_time.clone(), diff));
1312                            }
1313
1314                            for entry in &mut self.output_produced { (entry.0).1.join_assign(&meet); }
1315                            crate::consolidation::consolidate(&mut self.output_produced);
1316                        }
1317                    }
1318                }
1319            }
1320        }
1321    }
1322}