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