Skip to main content

hydro_lang/sim/
hooks.rs

1//! Test-side API for **simulator hooks**: scripting the decisions of unsafe operators.
2//!
3//! A hook handle (see [`crate::sim_hooks`]) is created from the
4//! [`FlowBuilder`](crate::compile::builder::FlowBuilder) via
5//! [`FlowBuilder::sim_hook`](crate::compile::builder::FlowBuilder::sim_hook) and attached
6//! to one specific unsafe operator with `nondet!(/** reason */ hook = handle)`. Inside a
7//! simulation test body (under [`SimFlow::deterministic`](crate::sim::flow::SimFlow::deterministic),
8//! [`fuzz`](crate::sim::flow::SimFlow::fuzz), or
9//! [`exhaustive`](crate::sim::flow::SimFlow::exhaustive)), the handle scripts the
10//! operator's decisions.
11//!
12//! # The script is a schedule
13//!
14//! Decision calls are `async`, and the sequence of calls in the test body is a
15//! **schedule**, read in program order:
16//!
17//! - Consecutive decisions that target *different hooks of the same tick* form a
18//!   **group**: one execution of that tick will consume all of them together.
19//! - A decision that targets a different tick — or the *next execution* of the same tick
20//!   (scripting a hook that already has a decision in the current group) — starts a new
21//!   group. The `.await` on the first decision of a new group suspends the test until the
22//!   previous group's tick execution has actually happened, so the test body advances in
23//!   lockstep with the execution it describes. Decisions whose turn has already come
24//!   return immediately without suspending.
25//! - Output awaits are group barriers: an output await completes only after every
26//!   decision scripted so far has been consumed.
27//!
28//! A decision may be scripted before its data exists (`release(3)` immediately after
29//! `send_many([1, 2, 3])`): the tick simply fires at the first moment the decision can be
30//! honored in full. A decision that can *never* be honored is reported when the
31//! simulation runs out of other work, attributed to the test line that is suspended
32//! waiting on it.
33//!
34//! # Holding data on purpose
35//!
36//! A hook with buffered data and no decision is an error the simulator reports at every
37//! scheduling boundary. When buffering *is* the scenario, declare it with the `pause`
38//! family ([`BatchHook::pause`], [`BatchHook::pause_while`],
39//! [`BatchHook::pause_until_count`], [`BatchHook::auto_pause`], and the snapshot
40//! equivalents).
41
42use std::future::Future;
43use std::marker::PhantomData;
44use std::pin::Pin;
45use std::task::{Context, Poll};
46
47use serde::Serialize;
48use serde::de::DeserializeOwned;
49
50use crate::live_collections::boundedness::{Bounded, Unbounded};
51use crate::live_collections::stream::{NoOrder, Ordering, Retries, TotalOrder};
52use crate::sim::compiled::{
53    ScheduleDecision, script_ctx, script_stuck_error, script_unconsumed_description,
54};
55use crate::sim::runtime::{
56    BatchDecision, InlineOrderingDecision, KeyedBatchDecision, KeyedSnapshotDecision,
57    MergeDecision, ScriptDecision, SnapshotDecision, TopLevelOrderingDecision,
58    UnorderedBatchDecision, UnorderedKeyedBatchDecision,
59};
60pub use crate::sim::runtime::{
61    BatchStatus, KeyedSnapshotStatus, MergeStatus, OrderingStatus, SnapshotStatus,
62};
63pub use crate::sim_hooks::{
64    BatchHook, KeyedBatchHook, KeyedMergeOrderedHook, KeyedOrderingHook, KeyedSnapshotHook,
65    MergeOrderedHook, OrderingHook, PartialOrderingHook, SimHook, SnapshotHook,
66};
67
68/// A scripted decision that has been issued but not yet installed into the schedule.
69///
70/// Awaiting it suspends the test until every previously scripted tick execution the
71/// decision must come after has actually happened (see the module docs); it resolves once
72/// the decision is installed for its tick's next execution. Panics (at the `.await`'s
73/// location) if the decision can never take its place in the schedule.
74#[must_use = "a scripted decision does nothing until awaited"]
75pub struct DecisionFuture {
76    hook_id: usize,
77    /// The decision, bincode-serialized (the handle and the hook it is bound to
78    /// statically know the same decision type). `None` once installed.
79    blob: Option<Vec<u8>>,
80}
81
82impl DecisionFuture {
83    fn new(hook_id: usize, decision: &impl ScriptDecision) -> Self {
84        DecisionFuture {
85            hook_id,
86            blob: Some(bincode::serialize(decision).unwrap()),
87        }
88    }
89}
90
91/// Panics (at the scripting call site) when a per-key decision names the same key more
92/// than once, establishing the no-duplicate-keys invariant of the keyed decisions before
93/// they are installed.
94#[track_caller]
95fn assert_distinct_keys<'a, K: std::hash::Hash + Eq + 'a>(
96    keys: impl Iterator<Item = &'a K>,
97    method: &str,
98) {
99    let mut seen: dfir_rs::rustc_hash::FxHashSet<&K> = Default::default();
100    for (position, key) in keys.enumerate() {
101        assert!(
102            seen.insert(key),
103            "{}: the same key appears more than once in a single decision (duplicate at entry {}); a key takes exactly one decision per tick",
104            method,
105            position
106        );
107    }
108}
109
110impl Future for DecisionFuture {
111    type Output = ();
112
113    #[track_caller]
114    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
115        let this = self.get_mut();
116        let Some(blob) = this.blob.take() else {
117            return Poll::Ready(());
118        };
119
120        let ctx = script_ctx();
121        match ctx.try_schedule_decision(this.hook_id, blob) {
122            Ok(ScheduleDecision::Installed) => Poll::Ready(()),
123            Ok(ScheduleDecision::Wait(blob)) => {
124                this.blob = Some(blob);
125                // While the script waits, the rest of the simulation does not: the
126                // scheduler keeps freely choosing which *other* ticks run. The test body
127                // is re-polled after every scheduler step; the waker is only needed for
128                // the parked (quiescent) case.
129                ctx.push_park_waker(cx.waker());
130                Poll::Pending
131            }
132            Err(message) => panic!("{}", message),
133        }
134    }
135}
136
137/// A `pause_until` wait: resolves once the hook's pending-input status satisfies the
138/// predicate, un-pausing the hook. The status is read from the hook **on demand** at every
139/// poll (the test body is polled between every pair of scheduler steps), so the wait
140/// resolves at the first scheduling point where the predicate holds. Panics (at the
141/// `.await`'s location) if the simulation can no longer satisfy it.
142#[must_use = "the pause is only released once this future is awaited"]
143pub struct PauseUntilFuture<S, F> {
144    hook_id: usize,
145    /// What the wait is called in error messages (e.g. `pause_until_count(3)`).
146    label: String,
147    predicate: F,
148    _status: PhantomData<fn(S)>,
149}
150
151impl<S: DeserializeOwned, F: Fn(&S) -> bool + Unpin> Future for PauseUntilFuture<S, F> {
152    type Output = ();
153
154    #[track_caller]
155    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
156        let this = self.get_mut();
157        let ctx = script_ctx();
158
159        // A `pause_until` wait is a script barrier, like an output await: it must not
160        // resolve (dropping the hold) while an earlier decision group is unconsumed,
161        // or the next decision would spuriously overlap the outstanding group and the
162        // boundary scan would see the exposed hook mid-group. And if that group is
163        // stuck, it is the root cause — report it instead of blaming the predicate.
164        if let Some(stuck) = script_unconsumed_description() {
165            if ctx.is_quiescent() {
166                panic!("{}", script_stuck_error(&stuck));
167            }
168            ctx.push_park_waker(cx.waker());
169            return Poll::Pending;
170        }
171
172        let hook = ctx.control(this.hook_id);
173
174        let status: S = bincode::deserialize(&hook.borrow().status_blob())
175            .expect("internal error: hook status blob did not match the handle's status type");
176
177        if (this.predicate)(&status) {
178            // The wait is satisfied; the hook is unpaused (the ordinary missing-decision
179            // error applies from here on).
180            hook.borrow_mut().release_hold();
181            Poll::Ready(())
182        } else if ctx.is_quiescent() {
183            let hook = hook.borrow();
184            let loc = hook.location_meta().location;
185            panic!(
186                "{} can never be satisfied: the hook at {} has {} and the simulation has no more work it can do",
187                this.label,
188                loc,
189                hook.describe_pending()
190                    .unwrap_or_else(|| "no pending input".to_owned()),
191            );
192        } else {
193            ctx.push_park_waker(cx.waker());
194            Poll::Pending
195        }
196    }
197}
198
199/// RAII guard for [`BatchHook::pause_while`] / [`SnapshotHook::pause_while`]: ends the
200/// hold when dropped (even on panic), leaving a standing `auto_pause` hold in place.
201struct PauseGuard {
202    hook_id: usize,
203}
204
205impl Drop for PauseGuard {
206    fn drop(&mut self) {
207        let ctx = script_ctx();
208        let hook = ctx.control(self.hook_id);
209        hook.borrow_mut().release_hold();
210    }
211}
212
213macro_rules! pause_family {
214    ($status:ty) => {
215        /// Declares that buffering at this operator is intended: while paused, the hook is
216        /// exempt from the missing-decision error, never causes its tick to run, and — if
217        /// its tick runs anyway because *other* hooks feed it — contributes its "nothing
218        /// new" behavior each time. Scripting any decision implicitly resumes the hook.
219        ///
220        /// A pause takes its place in the script like everything else: requested while a
221        /// decision is still pending, the hold begins once that decision has been
222        /// consumed.
223        pub fn pause(&self) {
224            let ctx = script_ctx();
225            ctx.control(self.id).borrow_mut().set_hold(true);
226        }
227
228        /// Ends a [`Self::pause`] (and clears [`Self::auto_pause`] mode).
229        pub fn resume(&self) {
230            let ctx = script_ctx();
231            let hook = ctx.control(self.id);
232            let mut hook = hook.borrow_mut();
233            hook.set_auto_pause(false);
234            hook.set_hold(false);
235        }
236
237        /// Sets a standing mode where this hook only ever acts when scripted: it holds
238        /// immediately, and every scripted decision leaves a fresh hold in place behind
239        /// it.
240        ///
241        /// This deliberately opts out of the forgotten-hook protection: if the test
242        /// forgets a step, the operator silently holds its data instead of failing. The
243        /// one `auto_pause()` line at the top of a test is the reviewer-visible marker
244        /// that this hook's timing is entirely script-driven, missed steps and all.
245        pub fn auto_pause(&self) {
246            let ctx = script_ctx();
247            let hook = ctx.control(self.id);
248            let mut hook = hook.borrow_mut();
249            hook.set_auto_pause(true);
250            hook.set_hold(true);
251        }
252
253        /// Pauses the hook exactly for the duration of `body` (resuming even on panic), so
254        /// a bracketed buffering phase cannot leak a paused hook.
255        pub async fn pause_while<Fut: Future>(&self, body: Fut) -> Fut::Output {
256            self.pause();
257            let _guard = PauseGuard { hook_id: self.id };
258            body.await
259        }
260
261        /// Pauses the hook and returns a future that resolves once the hook's
262        /// pending-input status satisfies `predicate` — a synchronization point for
263        /// scripts where the right decision is not knowable upfront. The status is read
264        /// on demand at every scheduling point. After the future resolves, the hook is
265        /// unpaused; the ordinary missing-decision error applies from there on.
266        pub fn pause_until(
267            &self,
268            predicate: impl Fn(&$status) -> bool + Unpin,
269        ) -> PauseUntilFuture<$status, impl Fn(&$status) -> bool + Unpin> {
270            self.pause_until_labeled("pause_until(..)".to_owned(), predicate)
271        }
272
273        fn pause_until_labeled<F: Fn(&$status) -> bool + Unpin>(
274            &self,
275            label: String,
276            predicate: F,
277        ) -> PauseUntilFuture<$status, F> {
278            let ctx = script_ctx();
279            ctx.control(self.id).borrow_mut().set_hold(true);
280            PauseUntilFuture {
281                hook_id: self.id,
282                label,
283                predicate,
284                _status: PhantomData,
285            }
286        }
287    };
288}
289
290impl<T, O: Ordering, R: Retries> BatchHook<T, O, R> {
291    pause_family!(BatchStatus);
292
293    /// Pauses the hook and returns a future that resolves once at least `n` elements are
294    /// buffered; see [`Self::pause_until`].
295    pub fn pause_until_count(
296        &self,
297        n: usize,
298    ) -> PauseUntilFuture<BatchStatus, impl Fn(&BatchStatus) -> bool + Unpin> {
299        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
300            status.buffered >= n
301        })
302    }
303}
304
305impl<T, R: Retries> BatchHook<T, TotalOrder, R>
306where
307    T: Serialize + DeserializeOwned + PartialEq,
308{
309    /// Scripts the next batch to be exactly the next `n` buffered elements. The tick
310    /// fires at the first moment the decision can be honored in full.
311    pub fn release(&self, n: usize) -> DecisionFuture {
312        DecisionFuture::new(self.id, &BatchDecision::<T>::Prefix(n))
313    }
314
315    /// Scripts the next batch to be exactly this sequence of values. Values must match the
316    /// buffered prefix in order: a mismatching available value panics immediately, while a
317    /// matching but incomplete prefix waits for the remaining values to arrive.
318    pub fn release_values(&self, values: impl IntoIterator<Item = T>) -> DecisionFuture {
319        DecisionFuture::new(
320            self.id,
321            &BatchDecision::Values(values.into_iter().collect()),
322        )
323    }
324
325    /// Scripts the next batch to be everything that has arrived by the time the tick
326    /// fires. Under fuzzing, the released contents co-vary with the schedule being
327    /// explored; use [`Self::release`] to name them exactly.
328    pub fn release_all(&self) -> DecisionFuture {
329        DecisionFuture::new(self.id, &BatchDecision::<T>::All)
330    }
331
332    /// Scripts the next batch to be empty, holding everything buffered. Shorthand for
333    /// [`Self::release`]`(0)`.
334    pub fn release_empty(&self) -> DecisionFuture {
335        self.release(0)
336    }
337}
338
339impl<T, R: Retries> BatchHook<T, NoOrder, R>
340where
341    T: Serialize + DeserializeOwned + PartialEq,
342{
343    /// Scripts the next batch to contain exactly this multiset of buffered values. Values
344    /// are matched independently of arrival order; duplicates request the corresponding
345    /// number of equal buffered items. The tick fires once every requested value exists.
346    pub fn release_values(&self, values: impl IntoIterator<Item = T>) -> DecisionFuture {
347        DecisionFuture::new(
348            self.id,
349            &UnorderedBatchDecision::Values(values.into_iter().collect()),
350        )
351    }
352
353    /// Scripts the next batch to be everything that has arrived by the time the tick
354    /// fires. Under fuzzing, the released contents co-vary with the schedule being
355    /// explored; use [`Self::release_values`] to name them exactly.
356    pub fn release_all(&self) -> DecisionFuture {
357        DecisionFuture::new(self.id, &UnorderedBatchDecision::<T>::All)
358    }
359
360    /// Scripts the next batch to be empty, holding everything buffered. Shorthand for
361    /// [`Self::release_values`] with no values.
362    pub fn release_empty(&self) -> DecisionFuture {
363        self.release_values([])
364    }
365}
366
367impl<T> OrderingHook<T, Unbounded>
368where
369    T: Serialize + DeserializeOwned,
370{
371    /// Scripts a top-level `assume_ordering` action to release the buffered element equal
372    /// to `value`. Exactly one element is released, preserving opportunities for ticks and
373    /// feedback to interleave with the remaining buffered input.
374    pub fn next(&self, value: T) -> DecisionFuture {
375        DecisionFuture::new(self.id, &TopLevelOrderingDecision::Next(value))
376    }
377
378    pause_family!(OrderingStatus);
379
380    /// Pauses a top-level ordering hook until at least `n` elements are buffered; see
381    /// [`Self::pause_until`].
382    pub fn pause_until_count(
383        &self,
384        n: usize,
385    ) -> PauseUntilFuture<OrderingStatus, impl Fn(&OrderingStatus) -> bool + Unpin> {
386        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
387            status.buffered >= n
388        })
389    }
390}
391
392impl<T> OrderingHook<T, Bounded>
393where
394    T: Serialize + DeserializeOwned,
395{
396    /// Scripts an in-tick `assume_ordering` observation to consume its complete input in
397    /// exactly this order. The supplied values must be a permutation of all values received
398    /// by the operator during that tick.
399    pub fn order(&self, values: impl IntoIterator<Item = T>) -> DecisionFuture {
400        DecisionFuture::new(
401            self.id,
402            &InlineOrderingDecision::Order(values.into_iter().collect()),
403        )
404    }
405}
406
407impl<T> SnapshotHook<T> {
408    /// Scripts the next tick execution to observe the buffered version equal to `value`:
409    /// scans forward from the currently-revealed version through the buffered ones and
410    /// releases the first equal version, skipping over earlier versions.
411    ///
412    /// This is a combined assertion and release, and the recommended way to script
413    /// snapshots: a script written with positional decisions breaks silently when the
414    /// program changes how often the state updates, while `reveal(value)` names the state
415    /// it means and any mis-synchronization fails loudly at the reveal.
416    pub fn reveal(&self, value: T) -> DecisionFuture
417    where
418        T: Serialize + DeserializeOwned,
419    {
420        DecisionFuture::new(self.id, &SnapshotDecision::Reveal(value))
421    }
422
423    /// Scripts the next tick execution to observe the next buffered version.
424    pub fn reveal_next(&self) -> DecisionFuture
425    where
426        T: Serialize + DeserializeOwned,
427    {
428        DecisionFuture::new(self.id, &SnapshotDecision::<T>::RevealNext)
429    }
430
431    /// Scripts the next tick execution to observe the newest version that has arrived by
432    /// the time the tick fires. Under fuzzing, which version is newest co-varies with the
433    /// schedule being explored; use [`Self::reveal`] to name it exactly.
434    pub fn reveal_latest(&self) -> DecisionFuture
435    where
436        T: Serialize + DeserializeOwned,
437    {
438        DecisionFuture::new(self.id, &SnapshotDecision::<T>::RevealLatest)
439    }
440
441    /// Scripts the next tick execution to observe the previously revealed version again.
442    pub fn keep(&self) -> DecisionFuture
443    where
444        T: Serialize + DeserializeOwned,
445    {
446        DecisionFuture::new(self.id, &SnapshotDecision::<T>::Keep)
447    }
448
449    pause_family!(SnapshotStatus);
450
451    /// Pauses the hook and returns a future that resolves once at least `n` newer
452    /// versions are buffered; see [`Self::pause_until`].
453    pub fn pause_until_versions(
454        &self,
455        n: usize,
456    ) -> PauseUntilFuture<SnapshotStatus, impl Fn(&SnapshotStatus) -> bool + Unpin> {
457        self.pause_until_labeled(format!("pause_until_versions({})", n), move |status| {
458            status.newer_versions >= n
459        })
460    }
461}
462
463impl<K, V, O: Ordering, R: Retries> KeyedBatchHook<K, V, O, R> {
464    pause_family!(BatchStatus);
465
466    /// Pauses the hook and returns a future that resolves once at least `n` entries are
467    /// buffered (in total, across all keys); see [`Self::pause_until`].
468    pub fn pause_until_count(
469        &self,
470        n: usize,
471    ) -> PauseUntilFuture<BatchStatus, impl Fn(&BatchStatus) -> bool + Unpin> {
472        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
473            status.buffered >= n
474        })
475    }
476}
477
478impl<K, V, R: Retries> KeyedBatchHook<K, V, TotalOrder, R>
479where
480    K: Serialize + DeserializeOwned + PartialEq,
481    V: Serialize + DeserializeOwned + PartialEq,
482{
483    /// Scripts the next batch to be exactly the next `count` buffered values of each
484    /// named key. The tick fires at the first moment the decision can be honored in
485    /// full.
486    ///
487    /// # Panics
488    /// Panics immediately if `counts` names the same key more than once.
489    #[track_caller]
490    pub fn release(&self, counts: impl IntoIterator<Item = (K, usize)>) -> DecisionFuture
491    where
492        K: std::hash::Hash + Eq,
493    {
494        let counts: Vec<(K, usize)> = counts.into_iter().collect();
495        assert_distinct_keys(counts.iter().map(|(key, _)| key), "release");
496        DecisionFuture::new(self.id, &KeyedBatchDecision::<K, V>::Prefixes(counts))
497    }
498
499    /// Scripts the next batch to be exactly these `(key, value)` entries. Each key's
500    /// values must match that key's buffered prefix in order (the interleaving of
501    /// *different* keys in the scripted sequence is irrelevant): a mismatching available
502    /// value panics immediately, while a matching but incomplete prefix waits for the
503    /// remaining values to arrive.
504    pub fn release_values(&self, entries: impl IntoIterator<Item = (K, V)>) -> DecisionFuture {
505        DecisionFuture::new(
506            self.id,
507            &KeyedBatchDecision::Values(entries.into_iter().collect()),
508        )
509    }
510
511    /// Scripts the next batch to be everything that has arrived by the time the tick
512    /// fires. Under fuzzing, the released contents co-vary with the schedule being
513    /// explored; use [`Self::release_values`] to name them exactly.
514    pub fn release_all(&self) -> DecisionFuture {
515        DecisionFuture::new(self.id, &KeyedBatchDecision::<K, V>::All)
516    }
517
518    /// Scripts the next batch to be empty, holding everything buffered. Shorthand for
519    /// [`Self::release_values`] with no entries.
520    pub fn release_empty(&self) -> DecisionFuture {
521        self.release_values([])
522    }
523}
524
525impl<K, V, R: Retries> KeyedBatchHook<K, V, NoOrder, R>
526where
527    K: Serialize + DeserializeOwned + PartialEq,
528    V: Serialize + DeserializeOwned + PartialEq,
529{
530    /// Scripts the next batch to contain exactly these `(key, value)` entries. Values are
531    /// matched per key as multisets (independently of arrival order); duplicates request
532    /// the corresponding number of equal buffered items. The tick fires once every
533    /// requested entry exists.
534    pub fn release_values(&self, entries: impl IntoIterator<Item = (K, V)>) -> DecisionFuture {
535        DecisionFuture::new(
536            self.id,
537            &UnorderedKeyedBatchDecision::Values(entries.into_iter().collect()),
538        )
539    }
540
541    /// Scripts the next batch to be everything that has arrived by the time the tick
542    /// fires. Under fuzzing, the released contents co-vary with the schedule being
543    /// explored; use [`Self::release_values`] to name them exactly.
544    pub fn release_all(&self) -> DecisionFuture {
545        DecisionFuture::new(self.id, &UnorderedKeyedBatchDecision::<K, V>::All)
546    }
547
548    /// Scripts the next batch to be empty, holding everything buffered. Shorthand for
549    /// [`Self::release_values`] with no entries.
550    pub fn release_empty(&self) -> DecisionFuture {
551        self.release_values([])
552    }
553}
554
555impl<K, V> KeyedSnapshotHook<K, V> {
556    /// Scripts the next tick execution to observe, for each named key, the buffered
557    /// version equal to the named value: scans forward from that key's currently-revealed
558    /// version through the buffered ones and releases the first equal version, skipping
559    /// over earlier versions. Keys that are not named observe their previously revealed
560    /// version again (or stay absent if they have never been revealed).
561    ///
562    /// This is a combined assertion and release, and the recommended way to script keyed
563    /// snapshots: naming the state each key means makes any mis-synchronization fail
564    /// loudly at the reveal.
565    ///
566    /// # Panics
567    /// Panics immediately if `entries` names the same key more than once: a key observes
568    /// exactly one version per tick execution.
569    #[track_caller]
570    pub fn reveal(&self, entries: impl IntoIterator<Item = (K, V)>) -> DecisionFuture
571    where
572        K: Serialize + DeserializeOwned + std::hash::Hash + Eq,
573        V: Serialize + DeserializeOwned,
574    {
575        let entries: Vec<(K, V)> = entries.into_iter().collect();
576        assert_distinct_keys(entries.iter().map(|(key, _)| key), "reveal");
577        DecisionFuture::new(self.id, &KeyedSnapshotDecision::Reveal(entries))
578    }
579
580    /// Scripts the next tick execution to observe, for every key, the newest version that
581    /// has arrived by the time the tick fires (keys with nothing newer observe their
582    /// previously revealed version again). Under fuzzing, which versions are newest
583    /// co-varies with the schedule being explored; use [`Self::reveal`] to name them
584    /// exactly.
585    pub fn reveal_latest(&self) -> DecisionFuture
586    where
587        K: Serialize + DeserializeOwned,
588        V: Serialize + DeserializeOwned,
589    {
590        DecisionFuture::new(self.id, &KeyedSnapshotDecision::<K, V>::RevealLatest)
591    }
592
593    /// Scripts the next tick execution to observe every key's previously revealed version
594    /// again.
595    pub fn keep(&self) -> DecisionFuture
596    where
597        K: Serialize + DeserializeOwned,
598        V: Serialize + DeserializeOwned,
599    {
600        DecisionFuture::new(self.id, &KeyedSnapshotDecision::<K, V>::Keep)
601    }
602
603    pause_family!(KeyedSnapshotStatus);
604
605    /// Pauses the hook and returns a future that resolves once at least `n` newer
606    /// versions are buffered (in total, across all keys); see [`Self::pause_until`].
607    pub fn pause_until_versions(
608        &self,
609        n: usize,
610    ) -> PauseUntilFuture<KeyedSnapshotStatus, impl Fn(&KeyedSnapshotStatus) -> bool + Unpin> {
611        self.pause_until_labeled(format!("pause_until_versions({})", n), move |status| {
612            status.newer_versions >= n
613        })
614    }
615}
616
617impl<K, V> KeyedOrderingHook<K, V, Unbounded>
618where
619    K: Serialize + DeserializeOwned,
620    V: Serialize + DeserializeOwned,
621{
622    /// Scripts a top-level keyed `assume_ordering` action to release the buffered entry
623    /// under `key` equal to `value`. Exactly one entry is released, preserving
624    /// opportunities for ticks and feedback to interleave with the remaining buffered
625    /// input.
626    pub fn next(&self, key: K, value: V) -> DecisionFuture {
627        DecisionFuture::new(self.id, &TopLevelOrderingDecision::Next((key, value)))
628    }
629
630    pause_family!(OrderingStatus);
631
632    /// Pauses a top-level keyed ordering hook until at least `n` entries are buffered (in
633    /// total, across all keys); see [`Self::pause_until`].
634    pub fn pause_until_count(
635        &self,
636        n: usize,
637    ) -> PauseUntilFuture<OrderingStatus, impl Fn(&OrderingStatus) -> bool + Unpin> {
638        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
639            status.buffered >= n
640        })
641    }
642}
643
644impl<K, V> KeyedOrderingHook<K, V, Bounded>
645where
646    K: Serialize + DeserializeOwned,
647    V: Serialize + DeserializeOwned,
648{
649    /// Scripts an in-tick keyed `assume_ordering` observation to consume its complete
650    /// input with each key's values in exactly the scripted per-key order. The supplied
651    /// entries must contain exactly all `(key, value)` entries received by the operator
652    /// during that tick; the relative order of *different* keys in the scripted sequence
653    /// is irrelevant (a keyed stream carries no cross-key ordering).
654    pub fn order(&self, entries: impl IntoIterator<Item = (K, V)>) -> DecisionFuture {
655        DecisionFuture::new(
656            self.id,
657            &InlineOrderingDecision::Order(entries.into_iter().collect()),
658        )
659    }
660}
661
662impl<K, V> PartialOrderingHook<K, V, Unbounded>
663where
664    K: Serialize + DeserializeOwned,
665    V: Serialize + DeserializeOwned,
666{
667    /// Scripts a top-level `entries_partially_ordered` action to release the front entry
668    /// of `key`'s buffer, which must equal `value` (within-key order is preserved, so a
669    /// mismatch panics). Exactly one entry is released, preserving opportunities for
670    /// ticks and feedback to interleave with the remaining buffered input.
671    pub fn next(&self, key: K, value: V) -> DecisionFuture {
672        DecisionFuture::new(self.id, &TopLevelOrderingDecision::Next((key, value)))
673    }
674
675    pause_family!(OrderingStatus);
676
677    /// Pauses a top-level partially-ordered hook until at least `n` entries are buffered
678    /// (in total, across all keys); see [`Self::pause_until`].
679    pub fn pause_until_count(
680        &self,
681        n: usize,
682    ) -> PauseUntilFuture<OrderingStatus, impl Fn(&OrderingStatus) -> bool + Unpin> {
683        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
684            status.buffered >= n
685        })
686    }
687}
688
689impl<K, V> PartialOrderingHook<K, V, Bounded>
690where
691    K: Serialize + DeserializeOwned,
692    V: Serialize + DeserializeOwned,
693{
694    /// Scripts an in-tick `entries_partially_ordered` observation to consume its complete
695    /// input in exactly this interleaving. The supplied entries must be a permutation of
696    /// all `(key, value)` entries received by the operator during that tick that preserves
697    /// each key's within-key order.
698    pub fn order(&self, entries: impl IntoIterator<Item = (K, V)>) -> DecisionFuture {
699        DecisionFuture::new(
700            self.id,
701            &InlineOrderingDecision::Order(entries.into_iter().collect()),
702        )
703    }
704}
705
706impl<T> MergeOrderedHook<T, Unbounded>
707where
708    T: Serialize + DeserializeOwned,
709{
710    /// Scripts a top-level `merge_ordered` action to release the front element of the
711    /// *first* input's buffer, which must equal `value` (per-input order is preserved, so
712    /// a mismatch panics). Exactly one element is released, preserving opportunities for
713    /// ticks and feedback to interleave with the remaining buffered input.
714    pub fn next_first(&self, value: T) -> DecisionFuture {
715        DecisionFuture::new(self.id, &MergeDecision::<T>::First(value))
716    }
717
718    /// Scripts a top-level `merge_ordered` action to release the front element of the
719    /// *second* input's buffer, which must equal `value`; see [`Self::next_first`].
720    pub fn next_second(&self, value: T) -> DecisionFuture {
721        DecisionFuture::new(self.id, &MergeDecision::<T>::Second(value))
722    }
723
724    /// Scripts a top-level `merge_ordered` action to release the front element of the
725    /// *first* input's buffer, whatever it is (waiting for one to arrive if that input is
726    /// empty). Unlike [`Self::next_first`], this does not assert the released value; use
727    /// `next_first(value)` to name it exactly and fail loudly on mis-synchronization.
728    pub fn advance_first(&self) -> DecisionFuture {
729        DecisionFuture::new(self.id, &MergeDecision::<T>::FirstNext(()))
730    }
731
732    /// Scripts a top-level `merge_ordered` action to release the front element of the
733    /// *second* input's buffer, whatever it is; see [`Self::advance_first`].
734    pub fn advance_second(&self) -> DecisionFuture {
735        DecisionFuture::new(self.id, &MergeDecision::<T>::SecondNext(()))
736    }
737
738    pause_family!(MergeStatus);
739
740    /// Pauses a top-level merge hook until at least `n` elements are buffered (in total,
741    /// across both inputs); see [`Self::pause_until`].
742    pub fn pause_until_count(
743        &self,
744        n: usize,
745    ) -> PauseUntilFuture<MergeStatus, impl Fn(&MergeStatus) -> bool + Unpin> {
746        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
747            status.first_buffered + status.second_buffered >= n
748        })
749    }
750}
751
752impl<T> MergeOrderedHook<T, Bounded>
753where
754    T: Serialize + DeserializeOwned,
755{
756    /// Scripts an in-tick `merge_ordered` observation to consume its complete input in
757    /// exactly this interleaving, with each value labeled by the input it is drawn from
758    /// (`false` = first/left, `true` = second/right). Each input's labeled values must be
759    /// exactly that input's tick-local batch, in order.
760    pub fn order(&self, values: impl IntoIterator<Item = (bool, T)>) -> DecisionFuture {
761        DecisionFuture::new(
762            self.id,
763            &InlineOrderingDecision::Order(values.into_iter().collect()),
764        )
765    }
766}
767
768impl<K, V> KeyedMergeOrderedHook<K, V, Unbounded>
769where
770    K: Serialize + DeserializeOwned,
771    V: Serialize + DeserializeOwned,
772{
773    /// Scripts a top-level keyed `merge_ordered` action to release the front entry of
774    /// `key`'s buffer in the *first* input, which must equal `value` (per-input
775    /// within-key order is preserved, so a mismatch panics). Exactly one entry is
776    /// released, preserving opportunities for ticks and feedback to interleave with the
777    /// remaining buffered input.
778    pub fn next_first(&self, key: K, value: V) -> DecisionFuture {
779        DecisionFuture::new(self.id, &MergeDecision::<(K, V), K>::First((key, value)))
780    }
781
782    /// Scripts a top-level keyed `merge_ordered` action to release the front entry of
783    /// `key`'s buffer in the *second* input, which must equal `value`; see
784    /// [`Self::next_first`].
785    pub fn next_second(&self, key: K, value: V) -> DecisionFuture {
786        DecisionFuture::new(self.id, &MergeDecision::<(K, V), K>::Second((key, value)))
787    }
788
789    /// Scripts a top-level keyed `merge_ordered` action to release the front entry of
790    /// `key`'s buffer in the *first* input, whatever its value (waiting for one to arrive
791    /// if that key's buffer is empty). Unlike [`Self::next_first`], this does not assert
792    /// the released value; use `next_first(key, value)` to name it exactly and fail
793    /// loudly on mis-synchronization.
794    pub fn advance_first(&self, key: K) -> DecisionFuture {
795        DecisionFuture::new(self.id, &MergeDecision::<(K, V), K>::FirstNext(key))
796    }
797
798    /// Scripts a top-level keyed `merge_ordered` action to release the front entry of
799    /// `key`'s buffer in the *second* input, whatever its value; see
800    /// [`Self::advance_first`].
801    pub fn advance_second(&self, key: K) -> DecisionFuture {
802        DecisionFuture::new(self.id, &MergeDecision::<(K, V), K>::SecondNext(key))
803    }
804
805    pause_family!(MergeStatus);
806
807    /// Pauses a top-level keyed merge hook until at least `n` entries are buffered (in
808    /// total, across both inputs and all keys); see [`Self::pause_until`].
809    pub fn pause_until_count(
810        &self,
811        n: usize,
812    ) -> PauseUntilFuture<MergeStatus, impl Fn(&MergeStatus) -> bool + Unpin> {
813        self.pause_until_labeled(format!("pause_until_count({})", n), move |status| {
814            status.first_buffered + status.second_buffered >= n
815        })
816    }
817}
818
819impl<K, V> KeyedMergeOrderedHook<K, V, Bounded>
820where
821    K: Serialize + DeserializeOwned,
822    V: Serialize + DeserializeOwned,
823{
824    /// Scripts an in-tick keyed `merge_ordered` observation to consume its complete input
825    /// in exactly this interleaving, with each `(key, value)` entry labeled by the input
826    /// it is drawn from (`false` = first/left, `true` = second/right). Each input's
827    /// labeled entries must be exactly that input's tick-local batch, with every key's
828    /// values in order; the relative order of *different* keys is irrelevant (a keyed
829    /// stream carries no cross-key ordering).
830    pub fn order(&self, entries: impl IntoIterator<Item = (bool, K, V)>) -> DecisionFuture {
831        DecisionFuture::new(
832            self.id,
833            &InlineOrderingDecision::Order(
834                entries
835                    .into_iter()
836                    .map(|(from_second, key, value)| (from_second, (key, value)))
837                    .collect(),
838            ),
839        )
840    }
841}