Skip to main content

differential_dataflow/operators/int_proxy/
reduce.rs

1//! The proxy reduce framework.
2//!
3//! A conventional differential reduce against ordered, copyable proxies.
4//! The backend supplies their interpretation.
5
6use super::pending::Pending;
7
8use timely::progress::{Antichain, Timestamp};
9use timely::progress::frontier::AntichainRef;
10
11use crate::difference::Semigroup;
12use crate::lattice::Lattice;
13use crate::trace::{Span, Description};
14use super::{KeyPosition, ProxyBridge};
15use crate::operators::reduce::{sort_dedup, ReduceTactic};
16use crate::operators::history::ValueHistory;
17
18/// A unit of proxied reduce work, presented to the backend.
19pub struct ReduceInstance<'a, T, B1, B2> {
20    /// The accumulated input history.
21    pub source_batches: &'a [B1],
22    /// The freshly arrived input delta.
23    pub input_batches: &'a [B1],
24    /// The accumulated output history.
25    pub output_batches: &'a [B2],
26    /// The compaction frontier for loading (the retire's lower bound).
27    pub lower: AntichainRef<'a, T>,
28}
29
30/// One window of the key space: the presentations a bounded, key-contiguous snip needs.
31///
32/// Seeds travel as times; records travel netted. The novel data's two roles are carried by two
33/// different channels: its TIME SUPPORT seeds interesting times and rides `seeds`, raw; its
34/// RECORDS are mere accumulants and join partners, so they ride `input` merged with the prior
35/// history, where they may net against it and be advanced like anything else. Consolidation can
36/// only cancel equal-`((key, id), time)` pairs, and such a time is necessarily in `seeds`, so no
37/// interesting time is lost to netting — the invariant that once forced the runs apart.
38///
39/// Owned by the harness and refilled by [`ProxyReduceBackend::next_window`].
40pub struct ReduceWindow<T, RIn, ROut, K = u64, VIn = u64, VOut = u64> {
41    /// The key's full input — novel and prior merged, netted — sorted & consolidated by `((key, value_id), time)`.
42    /// May be advanced to the compaction frontier.
43    pub input: ProxyBridge<T, RIn, K, VIn>,
44    /// The RAW novel time support: `(key, time)` pairs sorted by `(key, time)` and deduplicated.
45    /// Record these from the novel batches BEFORE any consolidation or advancement.
46    /// A netted-away record's time must still appear here.
47    pub seeds: Vec<(K, T)>,
48    /// Accumulated output preceding the retire's interval, same ordering as `input`.
49    pub output: ProxyBridge<T, ROut, K, VOut>,
50}
51
52impl<T, RIn, ROut, K, VIn, VOut> Default for ReduceWindow<T, RIn, ROut, K, VIn, VOut> {
53    fn default() -> Self { ReduceWindow { input: Vec::new(), seeds: Vec::new(), output: Vec::new() } }
54}
55
56impl<T, RIn, ROut, K, VIn, VOut> ReduceWindow<T, RIn, ROut, K, VIn, VOut> {
57    /// Clear the presentations, keeping their allocations.
58    pub fn clear(&mut self) {
59        self.input.clear();
60        self.seeds.clear();
61        self.output.clear();
62    }
63}
64
65/// The reduce backend: value semantics for a proxy-space reduction, driven by [`ProxyReduceTactic`].
66///
67/// The protocol for each round of invocation is
68/// `begin [ next_window reduce_corrections* emit ]* finish`,
69/// where the window loop runs until `next_window` reports the key space exhausted.
70pub trait ProxyReduceBackend<T, B1, B2> {
71    /// Independent groups, with identities stable across retires.
72    type Key: Copy + Ord;
73    /// Input value proxies, valid throughout a window.
74    type VIn: Copy + Ord;
75    /// Output value proxies, including newly produced values, valid throughout a window.
76    type VOut: Copy + Ord;
77    /// Diff type presented for the input.
78    type RIn: Semigroup;
79    /// Diff type of the output.
80    type ROut: Semigroup + 'static;
81
82    /// Initiate a session to create batches for these descriptions, which span `[lower, upper)`.
83    ///
84    /// It is the backend's job to prepare output batches for each of these descriptions.
85    /// The computation proceeds in windows of keys, where only the backend maintains this
86    /// work in progress, until `finish()` is called.
87    fn begin(&mut self, description: Description<T>);
88
89    /// Present the next window of the key space, and advance `from` past it.
90    ///
91    /// On entry `from` is `Start` or an inclusive `At(key)` lower bound on keys still to be covered.
92    /// The backend writes the window's exclusive upper bound as `At(key)`, or `End` when exhausted.
93    /// Each call must strictly advance `from`.
94    ///
95    /// The window must cover every key in `[from_before, from_after)` that carries a novel update or appears in `changed`.
96    /// For each key, present merged input (novel and prior together, netted), raw novel time support in `seeds`, and accumulated output.
97    /// A key must be reported entirely within the window that first mentions it: splitting one across windows drops the interaction between the halves.
98    /// `changed` is ascending; the harness reads no key outside the window's range, so a backend that keeps its own key order need not consult the whole space.
99    ///
100    /// `seeds` must be recorded from the novel batches before any consolidation or advancement:
101    /// a novel record that nets to zero against compacted history vanishes from `input`, but its
102    /// time must still seed — that cancellation is exactly the case that loses updates otherwise.
103    ///
104    /// The size of the window is up to the backend: large enough to amortize the crossings, small
105    /// enough that the presentations are affordable, as all are live at once.
106    fn next_window(
107        &mut self,
108        instance: &ReduceInstance<'_, T, B1, B2>,
109        changed: &[Self::Key],
110        from: &mut KeyPosition<Self::Key>,
111        window: &mut ReduceWindow<T, Self::RIn, Self::ROut, Self::Key, Self::VIn, Self::VOut>,
112    );
113
114    /// A wave of input-output reconciliation, in which the backend supplies necessary edits.
115    ///
116    /// Multiple keys are provided concurrently, for each an accumulated input and tentative output.
117    /// The backend should provide for each key the necessary output updates to bring the output in
118    /// with its desires. The `usize` integers upper bound the range for the corresponding key.
119    /// Each input and output bracket is sorted and consolidated by its value proxy.
120    fn reduce_corrections(
121        &mut self,
122        keys: &[Self::Key],
123        in_ends: &[usize],
124        input: &[(Self::VIn, Self::RIn)],
125        out_ends: &[usize],
126        output: &[(Self::VOut, Self::ROut)],
127    ) -> (Vec<(Self::VOut, Self::ROut)>, Vec<usize>);
128
129    /// Commit a collection of updates to the batch in progress.
130    fn emit(&mut self, records: &[((Self::Key, Self::VOut), T, Self::ROut)]);
131
132    /// Complete the session matching `begin`, yielding the batch it described,
133    /// or `None` when the span it described carries no updates.
134    fn finish(&mut self) -> Option<B2>;
135}
136
137/// A proxy-space [`ReduceTactic`]: matches input and output records by `key`.
138pub struct ProxyReduceTactic<T, Bk, K = u64> {
139    backend: Bk,
140    /// Maximum number of keys with live sweep state at once.
141    key_batch_size: usize,
142    /// Pending interesting times, shared across flat key ranges.
143    pending: Pending<T, K>,
144}
145
146impl<T, Bk, K> ProxyReduceTactic<T, Bk, K> {
147    /// A tactic deferring all value semantics to `backend`.
148    pub fn new(backend: Bk) -> Self {
149        ProxyReduceTactic { backend, key_batch_size: usize::MAX, pending: Pending::default() }
150    }
151
152    /// Limit simultaneous sweeps independently of the backend's presentation window.
153    ///
154    /// Complete proxy keys stay together, including all real keys sharing a proxy.
155    /// Sweep scratch is reused between groups within a retire. The bound does not
156    /// limit a single key's size, the presentation, or the output batch. Corrections
157    /// remain batched, and emission still happens once per backend window.
158    /// By default, all keys in the presentation can have live sweeps at once.
159    pub fn with_key_batch_size(mut self, key_batch_size: usize) -> Self {
160        assert!(key_batch_size > 0, "key batch size must be positive");
161        self.key_batch_size = key_batch_size;
162        self
163    }
164}
165
166impl<T, B1, B2, Bk, K: Copy + Ord> ReduceTactic<T, B1, B2> for ProxyReduceTactic<T, Bk, K>
167where
168    T: Timestamp + Lattice,
169    Bk: ProxyReduceBackend<T, B1, B2, Key = K>,
170{
171    fn retire(
172        &mut self,
173        source_batches: Vec<B1>,
174        output_batches: Vec<B2>,
175        input_batches: Vec<B1>,
176        lower: &Antichain<T>,
177        upper: &Antichain<T>,
178        held: &Antichain<T>,
179    ) -> (Option<Span<T, B2>>, Antichain<T>) {
180        if held.elements().iter().all(|t| upper.less_equal(t)) {
181            debug_assert!(
182                self.pending.frontier().iter().all(|time| held.less_equal(time)),
183                "held capabilities do not cover pending times",
184            );
185            return (None, held.clone());
186        }
187
188        let instance = ReduceInstance {
189            source_batches: &source_batches,
190            input_batches: &input_batches,
191            output_batches: &output_batches,
192            lower: lower.borrow(),
193        };
194
195        // Split the carried interesting times against `upper`.
196        // A time below it is DUE: its key must be re-evaluated this retire, so the key is `changed`.
197        // A time at or beyond it remains carried in `self.pending`.
198        // Activate time groups before visiting their keys. Due rows index a shared time table.
199        let due = self.pending.activate(upper.borrow());
200        // The keys the harness knows must be revisited. The backend adds those its novel batches
201        // touch, which it discovers while reading them; neither side scans the whole key space.
202        let mut changed: Vec<K> = due.rows.iter().map(|r| r.0).collect();
203        changed.dedup();
204        let mut deferred = Vec::new();
205        let mut due_pos = 0;
206
207        // Nothing due and nothing novel: no time in the interval can be interesting, so there is no
208        // work and no output. Return the frontier bounding the times still withheld — NOT an empty
209        // one. This is exactly where a due-only `changed` differs from the whole pending set: times
210        // beyond `upper` can remain when nothing is due, and releasing their capabilities would
211        // strand them (see the frontier clause of the `ReduceTactic::retire` contract).
212        if changed.is_empty() && instance.input_batches.is_empty() {
213            return (None, self.pending.frontier());
214        }
215
216        // The single output batch spans the retired interval.
217        let description = Description::new(lower.clone(), upper.clone(), Antichain::from_elem(T::minimum()));
218        self.backend.begin(description.clone());
219
220        // Start before every key; each window advances to an inclusive key bound or the end.
221        let mut from = KeyPosition::Start;
222        let mut window: ReduceWindow<T, Bk::RIn, Bk::ROut, K, Bk::VIn, Bk::VOut> = ReduceWindow::default();
223
224        // Retire-wide reusable scratch: cleared per group, window or wave, retaining capacity. Fresh
225        // per-key/per-wave `Vec`s were once the dominant cost here, which is why the slots and the
226        // staging buffers are held across the whole retire rather than built where they are used.
227        let mut slots: Vec<KeySweep<T, Bk::RIn, Bk::ROut, K, Bk::VIn, Bk::VOut>> = Vec::new();
228        let mut live: Vec<usize> = Vec::new();
229        let mut deltas: Vec<((K, Bk::VOut), T, Bk::ROut)> = Vec::new();
230        let mut batch_keys: Vec<K> = Vec::new();
231        let mut in_ends: Vec<usize> = Vec::new();
232        let mut in_all: Vec<(Bk::VIn, Bk::RIn)> = Vec::new();
233        let mut out_ends: Vec<usize> = Vec::new();
234        let mut out_all: Vec<(Bk::VOut, Bk::ROut)> = Vec::new();
235        let mut active: Vec<(usize, T)> = Vec::new();
236        let mut in_accum: Vec<(Bk::VIn, Bk::RIn)> = Vec::new();
237        let mut cur_out: Vec<(Bk::VOut, Bk::ROut)> = Vec::new();
238
239        while from != KeyPosition::End {
240            let before = from;
241            window.clear();
242            self.backend.next_window(&instance, &changed, &mut from, &mut window);
243            let p_in = &mut window.input;
244            let seeds = &window.seeds;
245            let p_out = &mut window.output;
246            super::debug_assert_sorted_bridge(p_in, "next_window.input");
247            super::debug_assert_sorted_bridge(p_out, "next_window.output");
248            debug_assert!(
249                seeds.windows(2).all(|w| w[0] < w[1]),
250                "next_window.seeds must be sorted by (key, time) and deduplicated",
251            );
252            // Without progress the window loop would never retire, so this guards liveness as well
253            // as contract; the range check catches a key reported outside the window that owns it,
254            // which would silently drop the interaction between its halves.
255            debug_assert!(
256                from > before,
257                "next_window must either advance `from` or report the key space exhausted",
258            );
259            debug_assert!(
260                {
261                    let mut keys = p_in.iter().map(|r| r.0.0).chain(seeds.iter().map(|s| s.0)).chain(p_out.iter().map(|r| r.0.0));
262                    keys.all(|k| before <= KeyPosition::At(k) && KeyPosition::At(k) < from)
263                },
264                "next_window must report a key entirely within the window that first mentions it",
265            );
266
267            deltas.clear();
268
269            // The window's keys are the proxies its presentations mention: the least of the three
270            // heads, each iteration, until all three are drained. A `changed` key that appears in
271            // none of them has no records at all, so its reduction has nothing to read and nothing
272            // to retract — the time its due moment would raise reaches the evaluation gate with an
273            // empty input and an empty output, and produces nothing. Skipping it is exactly what
274            // visiting it would do. (`changed` is still the backend's instruction about which keys
275            // to present; it is just not a source of keys here.)
276            //
277            // Each key gets a `Sweep`, which discovers and evaluates in ONE ascending pass,
278            // suspending where the conventional reduce would call user logic. Slots are reused
279            // across bounded groups as well as windows. A backend can present a large
280            // window without allocating sweep state for every key simultaneously.
281            let (mut is, mut ns, mut os) = (0usize, 0usize, 0usize);
282            while is < p_in.len() || ns < seeds.len() || os < p_out.len() {
283                let mut n_slots = 0usize;
284                live.clear();
285                // Mapped to keys before the min: the sources differ in shape.
286                while let Some(key) = [
287                    p_in.get(is).map(|record| record.0.0),
288                    seeds.get(ns).map(|seed| seed.0),
289                    p_out.get(os).map(|record| record.0.0),
290                ].into_iter().flatten().min() {
291                    let i0 = is;
292                    while is < p_in.len() && p_in[is].0.0 == key { is += 1; }
293                    let i1 = is;
294                    let n0 = ns;
295                    while ns < seeds.len() && seeds[ns].0 == key { ns += 1; }
296                    let n1 = ns;
297                    let o0 = os;
298                    while os < p_out.len() && p_out[os].0.0 == key { os += 1; }
299                    let o1 = os;
300
301                    if n_slots == slots.len() { slots.push(KeySweep::empty(key)); }
302                    let slot = &mut slots[n_slots];
303                    slot.key = key;
304                    slot.pended.clear();
305                    // Only the DUE times seed the sweep; the carried ones remain in `self.pending`.
306                    while due_pos < due.rows.len() && due.rows[due_pos].0 < key { due_pos += 1; }
307                    let start = due_pos;
308                    while due_pos < due.rows.len() && due.rows[due_pos].0 == key { due_pos += 1; }
309                    let owed = &due.rows[start..due_pos];
310                    let novel = &seeds[n0..n1];
311                    // One seed dominating every record has exactly one possible evaluation.
312                    let single = owed.first().map(|r| &due.times[r.1]).or_else(|| novel.first().map(|r| &r.1))
313                        .filter(|at| owed.len() <= 1 && novel.len() <= 1
314                            && novel.first().is_none_or(|r| &r.1 == *at)
315                            && p_in[i0..i1].iter().all(|r| r.1.less_equal(at))
316                            && p_out[o0..o1].iter().all(|r| r.1.less_equal(at)));
317                    slot.direct = single.map(|_| (i0..i1, o0..o1));
318                    slot.at = if let Some(at) = single {
319                        if upper.less_equal(at) { slot.pended.push(at.clone()); None }
320                        else { Some(at.clone()) }
321                    } else {
322                        slot.sweep.load(
323                            owed.iter().map(|&(_, row)| due.times[row].clone()),
324                            novel.iter().map(|r| r.1.clone()),
325                            p_in[i0..i1].iter_mut().map(|r| (r.0.1, std::mem::replace(&mut r.1, T::minimum()), r.2.clone())),
326                            p_out[o0..o1].iter_mut().map(|r| (r.0.1, std::mem::replace(&mut r.1, T::minimum()), r.2.clone())),
327                        );
328                        slot.sweep.next_crossing(upper, &mut slot.pended)
329                    };
330                    if slot.at.is_some() { live.push(n_slots); }
331                    else if !slot.pended.is_empty() {
332                        deferred.extend(slot.pended.drain(..).map(|time| (time, key)));
333                    }
334                    n_slots += 1;
335                    if n_slots == self.key_batch_size { break; }
336                }
337
338                // Each wave: read every suspended key's accumulations, cross the non-empty ones in one
339                // call, hand the corrections back, and step every live key on. A key retires when its
340                // sweep runs dry, at which point its pended times are carried forward.
341                while !live.is_empty() {
342                    batch_keys.clear();
343                    in_ends.clear();
344                    in_all.clear();
345                    out_ends.clear();
346                    out_all.clear();
347                    active.clear();
348
349                    for &si in live.iter() {
350                        let at = slots[si].at.clone().expect("live slots are suspended at a time");
351                        in_accum.clear();
352                        cur_out.clear();
353                        if let Some((ir, or)) = &slots[si].direct {
354                            in_accum.extend(p_in[ir.clone()].iter().map(|r| (r.0.1, r.2.clone())));
355                            cur_out.extend(p_out[or.clone()].iter().map(|r| (r.0.1, r.2.clone())));
356                            crate::consolidation::consolidate(&mut in_accum);
357                            crate::consolidation::consolidate(&mut cur_out);
358                        } else {
359                            slots[si].sweep.input_at(&at, &mut in_accum);
360                            slots[si].sweep.output_at(&at, &mut cur_out);
361                        }
362                        // An interesting time can still reach the gate with nothing to read; the
363                        // conventional reduce skips user logic there and so do we.
364                        if in_accum.is_empty() && cur_out.is_empty() { continue; }
365                        batch_keys.push(slots[si].key);
366                        in_all.append(&mut in_accum);
367                        in_ends.push(in_all.len());
368                        out_all.append(&mut cur_out);
369                        out_ends.push(out_all.len());
370                        active.push((si, at));
371                    }
372
373                    if !batch_keys.is_empty() {
374                        let (corr, corr_ends) = self.backend.reduce_corrections(&batch_keys, &in_ends, &in_all, &out_ends, &out_all);
375                        let mut cstart = 0usize;
376                        for (bi, (si, at)) in active.iter().enumerate() {
377                            let cend = corr_ends[bi];
378                            if cstart != cend {
379                                debug_assert!(held.elements().iter().any(|h| h.less_equal(at)), "no held capability <= active time");
380                                for (vid, d) in &corr[cstart..cend] {
381                                    deltas.push(((slots[*si].key, *vid), at.clone(), d.clone()));
382                                }
383                                if slots[*si].direct.is_none() {
384                                    slots[*si].sweep.commit(at, corr[cstart..cend].iter().cloned());
385                                }
386                            }
387                            cstart = cend;
388                        }
389                    }
390
391                    // Step every live key past the time it was suspended at, and retire the spent ones.
392                    for &si in live.iter() {
393                        let slot = &mut slots[si];
394                        slot.at = if slot.direct.is_some() { None }
395                            else { slot.sweep.next_crossing(upper, &mut slot.pended) };
396                        if slot.at.is_none() && !slot.pended.is_empty() {
397                            deferred.extend(slot.pended.drain(..).map(|time| (time, slot.key)));
398                        }
399                    }
400                    live.retain(|&si| slots[si].at.is_some());
401                }
402                // Bound staging by the same key groups as the sweep scratch.
403                self.pending.insert(std::mem::take(&mut deferred));
404            }
405
406            if !deltas.is_empty() {
407                crate::consolidation::consolidate_updates(&mut deltas);
408                self.backend.emit(&deltas[..]);
409            }
410        }
411
412        let produced = Some(Span::new(description, self.backend.finish()));
413        (produced, self.pending.frontier())
414    }
415}
416
417/// One key's slot in a window: its [`Sweep`], the time it is suspended at, and the times it has
418/// pended so far. Slots and their scratch capacity are reused across groups and windows
419/// within a retire, then dropped when the retire completes.
420struct KeySweep<T, RIn, ROut, K, VIn, VOut> {
421    key: K,
422    sweep: Sweep<T, RIn, ROut, VIn, VOut>,
423    direct: Option<(std::ops::Range<usize>, std::ops::Range<usize>)>,
424    /// Times at or beyond `upper` the sweep has reached; carried forward when the slot retires.
425    pended: Vec<T>,
426    /// The time the sweep last suspended at, or `None` once it is spent.
427    at: Option<T>,
428}
429
430impl<T: Timestamp + Lattice, RIn: Semigroup + Clone, ROut: Semigroup + Clone, K, VIn: Copy + Ord, VOut: Copy + Ord> KeySweep<T, RIn, ROut, K, VIn, VOut> {
431    fn empty(key: K) -> Self {
432        KeySweep { key, sweep: Sweep::new(), direct: None, pended: Vec::new(), at: None }
433    }
434}
435
436/// Updates an optional meet by an optional time.
437fn update_meet<T: Lattice + Clone>(meet: &mut Option<T>, other: Option<&T>) {
438    if let Some(time) = other {
439        match meet.as_mut() {
440            Some(m) => m.meet_assign(time),
441            None => *meet = Some(time.clone()),
442        }
443    }
444}
445
446/// A resumable, fused determination-and-evaluation sweep over one key's times.
447///
448/// A determination pass would enumerate a key's interesting times up front, and the caller would
449/// then walk them again to evaluate. The conventional reduce does not: it runs ONE ascending pass
450/// and evaluates as it discovers. It can, because discovery never looks backwards — every
451/// synthesized time is
452/// `next_time.join(t)` for some `t` NOT at or below `next_time`, so it is strictly greater, and new
453/// work only ever lands ahead of the sweep.
454///
455/// This is that pass, cut at the point where the conventional operator would call user logic. Each
456/// [`next_crossing`](Self::next_crossing) returns the next in-interval time that needs evaluating,
457/// with the buffers positioned to read the accumulations; the caller evaluates and hands the
458/// corrections back through [`commit`](Self::commit); the next call resumes. Many keys can be run
459/// to their next crossing and evaluated together, which is what a batched backend wants, without
460/// any of them enumerating their times first.
461///
462/// The schedule is `formal/Differential/RoundCoverage.lean`'s `round_coverage`: a time carrying an
463/// output change lies in the join-closure of `prior ∪ novel` AND is at or above some novel time.
464/// The novel times arrive as the SEED LIST (the harness's warned times merged with the window's raw
465/// novel time support); the records themselves travel merged with the prior history and carry no
466/// witness duty, which is what lets them net and advance. Coverage is invariant under that move:
467/// the witness clause reads only times, and consolidation cancels only equal-time pairs whose time
468/// the seed list retains.
469struct Sweep<T, RIn, ROut, VIn, VOut> {
470    /// The accumulated input (novel and prior, merged and netted) and output: join partners, and
471    /// the accumulations to evaluate over. Both may be advanced freely — witness duty lives in
472    /// `seeds`, not in any record.
473    input: ValueHistory<VIn, T, RIn>,
474    output: ValueHistory<VOut, T, ROut>,
475    /// The key's seed times — the harness's due (warned) times merged with the raw novel time
476    /// support — ascending and deduplicated, with their suffix meets; `seed_pos` consumes them.
477    /// These are the ONLY source of interest: the schedule is stated over them, so they are held
478    /// raw, never advanced.
479    seeds: Vec<T>,
480    seed_meets: Vec<T>,
481    seed_pos: usize,
482    /// Synthesized times not yet visited, sorted DESCENDING so `last()` is the least.
483    synth: Vec<T>,
484    /// The seed times reached so far, compacted by the running meet. They are the witnesses the
485    /// absorption test looks for, and the partners a close joins against; keeping them collapsed is
486    /// what stops a key with many reached times rescanning all of them.
487    reached: Vec<T>,
488    /// Scratch for one step's synthesized times.
489    temporary: Vec<T>,
490    /// Corrections emitted so far this sweep, meet-collapsed; both a join partner and part of the
491    /// output accumulation.
492    produced: Vec<((VOut, T), ROut)>,
493    /// The meet of every time still to come.
494    meet: Option<T>,
495    /// Whether the last `next_crossing` returned a time whose step is not yet settled.
496    suspended: bool,
497}
498
499/// What one [`tick`](Sweep::tick) decided about the time it visited.
500enum Tick<T> {
501    /// No seed reaches this time; the sweep moved past it.
502    Passed,
503    /// Reached, but at or beyond `upper`: carried to a later round rather than evaluated.
504    Pended,
505    /// Reached and in the interval. The caller must evaluate here before the sweep goes on.
506    Crossing(T),
507    /// Every source is drained.
508    Done,
509}
510
511impl<T: Timestamp + Lattice, RIn: Semigroup + Clone, ROut: Semigroup + Clone, VIn: Copy + Ord, VOut: Copy + Ord> Sweep<T, RIn, ROut, VIn, VOut> {
512    /// An empty sweep, to be `load`ed and reused for successive keys.
513    fn new() -> Self {
514        Sweep {
515            input: ValueHistory::new(), output: ValueHistory::new(),
516            seeds: Vec::new(), seed_meets: Vec::new(), seed_pos: 0,
517            synth: Vec::new(), reached: Vec::new(), temporary: Vec::new(),
518            produced: Vec::new(), meet: None, suspended: false,
519        }
520    }
521
522    /// Position the sweep at the start of one key.
523    ///
524    /// `owed` is the harness's due (warned) times and `novel_times` the window's raw novel time
525    /// support, both ascending; they merge into the seed list, which is held raw — the schedule is
526    /// stated over these times, so they are never advanced. The records in `input` are merged and
527    /// netted (novel and prior together): a cancelled record's time survives in the seed list, so
528    /// netting loses nothing, and every record is a mere partner/accumulant that the meet may
529    /// advance freely.
530    fn load(
531        &mut self,
532        owed: impl Iterator<Item = T>,
533        novel_times: impl Iterator<Item = T>,
534        input: impl Iterator<Item = (VIn, T, RIn)>,
535        output: impl Iterator<Item = (VOut, T, ROut)>,
536    ) {
537        // Merge the two ascending seed sources, deduplicated.
538        self.seeds.clear();
539        let mut owed = owed.peekable();
540        let mut novel = novel_times.peekable();
541        loop {
542            let take_owed = match (owed.peek(), novel.peek()) {
543                (Some(a), Some(b)) => a <= b,
544                (Some(_), None) => true,
545                (None, Some(_)) => false,
546                (None, None) => break,
547            };
548            let time = if take_owed { owed.next() } else { novel.next() }.expect("peeked");
549            if self.seeds.last() != Some(&time) {
550                self.seeds.push(time);
551            }
552        }
553        self.seed_meets.clear();
554        self.seed_meets.extend(self.seeds.iter().cloned());
555        for i in (1..self.seed_meets.len()).rev() {
556            let (init, tail) = self.seed_meets.split_at_mut(i);
557            init[i - 1].meet_assign(&tail[0]);
558        }
559        self.seed_pos = 0;
560        self.synth.clear();
561        self.reached.clear();
562        self.temporary.clear();
563        self.produced.clear();
564        self.suspended = false;
565
566        // The meet of every seed bounds every time the sweep will visit, so the record buffers can
567        // be advanced by it at load.
568        let mut meet: Option<T> = None;
569        update_meet(&mut meet, self.seed_meets.first());
570        self.input.load_iter(input, meet.as_ref());
571        self.output.load_iter(output, meet.as_ref());
572        self.meet = meet;
573    }
574
575    /// Advance to the next in-interval time that needs evaluating, or `None` once the key is spent.
576    ///
577    /// Times at or beyond `upper` that the schedule reaches are appended to `pended` for the caller
578    /// to carry into a later round.
579    fn next_crossing(&mut self, upper: &Antichain<T>, pended: &mut Vec<T>) -> Option<T> {
580        loop {
581            // A crossing leaves its step half-finished, because `settle` must see the corrections
582            // the caller commits. Finishing it is the first thing the next call does.
583            if self.suspended {
584                self.suspended = false;
585                self.settle();
586            }
587            match self.tick(upper, pended) {
588                Tick::Done => return None,
589                Tick::Crossing(at) => {
590                    self.suspended = true;
591                    return Some(at);
592                }
593                Tick::Passed | Tick::Pended => {}
594            }
595        }
596    }
597
598    /// Visit one time: find it, decide whether it is reached, close it forward, and report.
599    fn tick(&mut self, upper: &Antichain<T>, pended: &mut Vec<T>) -> Tick<T> {
600        let Some(at) = self.frontier() else { return Tick::Done };
601        let reached = self.absorb(&at);
602        if upper.less_equal(&at) {
603            // Out of the interval: nothing can be emitted here, so there is nothing to close
604            // against either — a join with `at` is at or beyond `at`, hence also out of interval,
605            // and will be rediscovered from `at` in the round that admits it.
606            self.settle();
607            if reached { pended.push(at); return Tick::Pended; }
608            return Tick::Passed;
609        }
610        self.close(&at, reached, upper, pended);
611        if reached { return Tick::Crossing(at); }
612        self.settle();
613        Tick::Passed
614    }
615
616    /// The sweep's position: the least time any source still offers.
617    ///
618    /// The TOTAL order, not the partial one. Every time `close` produces is strictly greater than
619    /// the position that produced it, so new work only ever lands ahead of here and the sweep never
620    /// revisits.
621    fn frontier(&self) -> Option<T> {
622        [
623            self.seeds.get(self.seed_pos), self.input.time(),
624            self.output.time(), self.synth.last(),
625        ].into_iter().flatten().min().cloned()
626    }
627
628    /// Step every source sitting at `at`, and decide whether `at` is REACHED.
629    ///
630    /// Reached is clause two of `round_coverage` — `∃ nu ∈ novel, nu ≤ at` — evaluated
631    /// incrementally: either a seed lands exactly here, or one already stepped in lies below.
632    ///
633    /// Input and output are stepped whether or not `at` is reached, and that is forced rather than
634    /// eager: they are sources of the frontier, so leaving them would stall the sweep, and their
635    /// edits must reach the buffers or they are lost to every later accumulation. Stepping only
636    /// moves an edit across; it consolidates nothing. The expensive part — `advance_buffer_by`,
637    /// which joins every buffered time and re-consolidates — is deferred to `close`, and happens
638    /// only where the buffers are actually read.
639    fn absorb(&mut self, at: &T) -> bool {
640        self.input.step_while_time_is(at);
641        self.output.step_while_time_is(at);
642
643        // A seed here — a due time or a novel-support time — is consumed into the reached set,
644        // where it becomes a witness and a join partner for every later time. So is a synthetic
645        // join scheduled for here.
646        let mut reached = false;
647        while self.synth.last() == Some(at) {
648            self.reached.push(self.synth.pop().expect("nonempty"));
649            reached = true;
650        }
651        while self.seeds.get(self.seed_pos) == Some(at) {
652            self.reached.push(at.clone());
653            self.seed_pos += 1;
654            reached = true;
655        }
656        // Absorption: a time at or above a seed already consumed is itself reached, because
657        // joining that seed with it yields it back.
658        reached || self.reached.iter().any(|t| t.less_equal(at))
659    }
660
661    /// Close `at` forward under joins — clause one of `round_coverage`, the join-closure.
662    ///
663    /// Against the REACHED (seed-derived) times always, reached or not: an unreached time joined
664    /// with a seed lands at or above that seed, so it carries a witness and is on the schedule.
665    ///
666    /// Against the PRIOR times only when `at` is itself reached, because the join then inherits
667    /// `at`'s witness. A join of two prior times carries none and is deliberately never produced —
668    /// that asymmetry is the whole of why an incremental operator does less work than the closure
669    /// of everything.
670    ///
671    /// `produced` counts as prior. A correction emitted at `p` changes the accumulated output at
672    /// every time at or above `p`, so `p ∨ at` has to be visited; nothing else covers it, since
673    /// this round's corrections are not in the output history and `at` was not yet stepped in when
674    /// the sweep passed `p`.
675    fn close(&mut self, at: &T, reached: bool, upper: &Antichain<T>, pended: &mut Vec<T>) {
676        self.temporary.extend(self.reached.iter()
677            .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
678        if reached {
679            if let Some(meet) = self.meet.as_ref() {
680                self.input.advance_buffer_by(meet);
681                self.output.advance_buffer_by(meet);
682            }
683            self.temporary.extend(self.input.buffer().iter().map(|((_, t), _)| t)
684                .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
685            self.temporary.extend(self.output.buffer().iter().map(|((_, t), _)| t)
686                .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
687            self.temporary.extend(self.produced.iter().map(|((_, t), _)| t)
688                .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
689        }
690        sort_dedup(&mut self.temporary);
691        let before = self.synth.len();
692        for time in self.temporary.drain(..) {
693            if upper.less_equal(&time) { pended.push(time); } else { self.synth.push(time); }
694        }
695        if self.synth.len() > before {
696            self.synth.sort_by(|x, y| y.cmp(x));
697            self.synth.dedup();
698        }
699    }
700
701    /// The input accumulation at the suspended time.
702    fn input_at(&self, at: &T, into: &mut Vec<(VIn, RIn)>) {
703        for ((id, time), diff) in self.input.buffer().iter() {
704            if time.less_equal(at) { into.push((*id, diff.clone())); }
705        }
706        crate::consolidation::consolidate(into);
707    }
708
709    /// The tentative output accumulation at the suspended time, including this sweep's corrections.
710    fn output_at(&self, at: &T, into: &mut Vec<(VOut, ROut)>) {
711        for ((id, time), diff) in self.output.buffer().iter().chain(self.produced.iter()) {
712            if time.less_equal(at) { into.push((*id, diff.clone())); }
713        }
714        crate::consolidation::consolidate(into);
715    }
716
717    /// Record the corrections evaluated at the suspended time, and collapse them by the meet.
718    fn commit(&mut self, at: &T, corrections: impl Iterator<Item = (VOut, ROut)>) {
719        let before = self.produced.len();
720        for (id, diff) in corrections { self.produced.push(((id, at.clone()), diff)); }
721        if self.produced.len() > before {
722            if let Some(meet) = self.meet.as_ref() {
723                for entry in self.produced.iter_mut() { (entry.0).1.join_assign(meet); }
724            }
725            crate::consolidation::consolidate(&mut self.produced);
726        }
727    }
728
729    /// Close a step: recompute the meet of everything still to come, and compact the reached set by
730    /// it. This is what keeps a key with a long history linear rather than quadratic.
731    fn settle(&mut self) {
732        let mut meet: Option<T> = None;
733        update_meet(&mut meet, self.input.meet());
734        update_meet(&mut meet, self.output.meet());
735        for time in self.synth.iter() { update_meet(&mut meet, Some(time)); }
736        update_meet(&mut meet, self.seed_meets.get(self.seed_pos));
737        if let Some(m) = meet.as_ref() {
738            for time in self.reached.iter_mut() { *time = time.join(m); }
739        }
740        sort_dedup(&mut self.reached);
741        self.meet = meet;
742    }
743}