Skip to main content

hydro_lang/sim/runtime/
observation.rs

1//! Top-level observation hooks ([`ObservationHook`]): hooks with no tick DFIR that are
2//! their own scheduling unit — running one *is* releasing data (e.g. `assume_ordering`
3//! on a non-tick stream, or a hooked top-level `fold`). Each is its own
4//! `SimObservation` in the scheduler. Their scripted decision/status types and
5//! [`ScriptableHook`] impls live alongside them.
6//!
7//! The retry kinds ([`TopLevelNoOrderStreamRetriesHook`], [`TopLevelOrderedStreamRetriesHook`],
8//! [`TopLevelAtLeastOnceOrderHook`]) implement only the scriptable surface, never
9//! [`ObservationHook`]: their decision spaces are infinite (every element admits
10//! arbitrarily many retries), so no autonomous exploration is possible and the builder
11//! requires them to be bound to a sim hook.
12
13use std::cell::RefCell;
14use std::collections::VecDeque;
15use std::hash::Hash;
16use std::rc::Rc;
17
18use bolero::generator::bolero_generator::driver::object::Borrowed;
19use bolero::{ValueGenerator, produce};
20use dfir_rs::rustc_hash::FxHashMap;
21use dfir_rs::util::unsync::mpsc::Sender;
22
23use super::{
24    HookLocationMeta, ObservationHook, RuntimeHook, ScriptDecision, ScriptableHook,
25    ScriptableObservationHook, TruncatedLabeledVecDebug, TruncatedVecDebug, abort,
26    describe_keyed_pending, keyed_buffer_len, log_release,
27};
28
29/// Top-level (outside-tick) `assume_ordering` hooks release elements **one at a
30/// time** rather than shuffling the entire batch. This is the key mechanism for
31/// simulating causality in feedback cycles: when data flows through a network hop
32/// and cycles back (e.g. via `forward_ref`), the cycled-back result can arrive
33/// and interleave with elements that are still pending in the input queue.
34///
35/// For example, given input `[1, 2, 3]` where each element is mapped and sent
36/// through a network cycle, the simulator can explore orderings like
37/// `[1, map(1), 2, map(2), 3, ...]` -- the cycled-back `map(1)` arrives before `2`.
38///
39/// This is the only place where such causality is observable, because top-level
40/// unbounded streams are "maximally async" -- any non-atomic stream can be
41/// arbitrarily decoupled. The in-tick variants (`StreamOrderHook`,
42/// `KeyedStreamOrderHook`) shuffle the full batch instead, since within a tick
43/// all data is available simultaneously.
44///
45/// The `sim_top_level_assume_ordering_*` tests are regression tests ensuring
46/// this one-at-a-time release behavior correctly explores causal interleavings.
47pub struct TopLevelStreamOrderHook<T> {
48    pub input: Rc<RefCell<VecDeque<T>>>,
49    pub to_release: Option<Vec<T>>,
50    pub output: Sender<T>,
51    pub location: HookLocationMeta,
52    pub format_item_debug: fn(&T) -> Option<String>,
53}
54
55impl<T> RuntimeHook for TopLevelStreamOrderHook<T> {
56    fn has_pending_input(&self) -> bool {
57        !self.input.borrow().is_empty()
58    }
59
60    fn only_one_possible_decision(&self) -> bool {
61        // A sole buffered element has exactly one possible release; ordering only
62        // becomes a choice with two or more.
63        self.input.borrow().len() <= 1
64    }
65
66    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
67        if let Some(to_release) = self.to_release.take() {
68            if !to_release.is_empty()
69                && let Some(log_writer) = log_writer
70            {
71                let HookLocationMeta {
72                    location: batch_location,
73                    line,
74                    caret_indent,
75                } = self.location;
76                let note_str = format!(
77                    "^ observed non-deterministic order: {:?}",
78                    TruncatedVecDebug(
79                        RefCell::new(Some(to_release.iter())),
80                        8,
81                        self.format_item_debug
82                    )
83                );
84
85                let _ = writeln!(log_writer);
86                log_release(
87                    log_writer,
88                    batch_location,
89                    line,
90                    caret_indent,
91                    &note_str,
92                    colored::Color::Green,
93                );
94            }
95
96            for item in to_release {
97                self.output.try_send(item).unwrap();
98            }
99        } else {
100            panic!("No decision to release");
101        }
102    }
103
104    fn location_meta(&self) -> HookLocationMeta {
105        self.location
106    }
107}
108
109impl<T> ObservationHook for TopLevelStreamOrderHook<T> {
110    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
111        let mut current_input = self.input.borrow_mut();
112        // Instead of a full shuffle, we only release one element at a time
113        // in order to handle possible feedback cycles.
114        let idx = (0..current_input.len()).generate(driver).unwrap();
115        let item = current_input.remove(idx).unwrap();
116        self.to_release = Some(vec![item]);
117    }
118}
119
120/// A scripted decision for a top-level `assume_ordering` observation.
121#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
122pub enum TopLevelOrderingDecision<T> {
123    Next(T),
124}
125
126impl<T> ScriptDecision for TopLevelOrderingDecision<T>
127where
128    T: serde::Serialize + serde::de::DeserializeOwned,
129{
130    fn describe(&self) -> String {
131        "next(value)".to_owned()
132    }
133}
134
135#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
136pub struct OrderingStatus {
137    pub buffered: usize,
138}
139
140impl<T> ScriptableHook for TopLevelStreamOrderHook<T>
141where
142    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
143{
144    type Decision = TopLevelOrderingDecision<T>;
145    type Status = OrderingStatus;
146
147    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
148        let TopLevelOrderingDecision::Next(expected) = decision;
149        Ok(self.input.borrow().iter().any(|item| item == expected))
150    }
151
152    fn apply(&mut self, decision: Self::Decision) {
153        let mut input = self.input.borrow_mut();
154        let TopLevelOrderingDecision::Next(expected) = decision;
155        let index = input.iter().position(|item| item == &expected).unwrap();
156        self.to_release = Some(vec![input.remove(index).unwrap()]);
157    }
158
159    fn implicit(&mut self) {
160        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
161        // hooks; a top-level observation consists of exactly this hook, so it can never
162        // be forced to run without a scripted decision.
163        abort!("implicit decision invoked on a top-level ordering hook");
164    }
165
166    fn status(&self) -> Self::Status {
167        OrderingStatus {
168            buffered: self.input.borrow().len(),
169        }
170    }
171
172    fn describe_pending(&self) -> Option<String> {
173        let input = self.input.borrow();
174        (!input.is_empty()).then(|| {
175            format!(
176                "{} buffered ordering item(s): {:?}",
177                input.len(),
178                TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
179            )
180        })
181    }
182}
183
184impl<T> ScriptableObservationHook for TopLevelStreamOrderHook<T> where
185    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq
186{
187}
188
189/// Hook for top-level folds. Selects a non-empty subset of buffered inputs to release,
190/// always permuting them to explore all orderings. Unselected elements remain
191/// in the buffer for future releases (modeling delayed/lossy inputs).
192pub struct TopLevelFoldHook<T> {
193    pub input: Rc<RefCell<VecDeque<T>>>,
194    pub to_release: Option<Vec<T>>,
195    pub output: Sender<Vec<T>>,
196    pub location: HookLocationMeta,
197    pub format_item_debug: fn(&T) -> Option<String>,
198}
199
200impl<T> RuntimeHook for TopLevelFoldHook<T> {
201    fn has_pending_input(&self) -> bool {
202        !self.input.borrow().is_empty()
203    }
204
205    fn only_one_possible_decision(&self) -> bool {
206        // The subset must be non-empty, so a sole buffered element is forced; subset
207        // choice and permutation only appear with two or more.
208        self.input.borrow().len() <= 1
209    }
210
211    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
212        if let Some(to_release) = self.to_release.take() {
213            if !to_release.is_empty()
214                && let Some(log_writer) = log_writer
215            {
216                let HookLocationMeta {
217                    location: batch_location,
218                    line,
219                    caret_indent,
220                } = self.location;
221                let note_str = format!(
222                    "^ fold input batch (permuted): {:?}",
223                    TruncatedVecDebug(
224                        RefCell::new(Some(to_release.iter())),
225                        8,
226                        self.format_item_debug
227                    )
228                );
229
230                let _ = writeln!(log_writer);
231                log_release(
232                    log_writer,
233                    batch_location,
234                    line,
235                    caret_indent,
236                    &note_str,
237                    colored::Color::Green,
238                );
239            }
240
241            self.output.try_send(to_release).unwrap();
242        } else {
243            panic!("No decision to release");
244        }
245    }
246
247    fn location_meta(&self) -> HookLocationMeta {
248        self.location
249    }
250}
251
252impl<T> ObservationHook for TopLevelFoldHook<T> {
253    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
254        let mut current_input = self.input.borrow_mut();
255
256        // Select a non-empty subset: for each element, decide include/exclude.
257        // Only force inclusion on the last element if nothing was selected yet.
258        let mut selected = Vec::new();
259        let mut remaining = VecDeque::new();
260
261        let len = current_input.len();
262        for (i, item) in current_input.drain(..).enumerate() {
263            let is_last = i == len - 1;
264            let must_include = is_last && selected.is_empty();
265            if must_include || produce().generate(driver).unwrap() {
266                selected.push(item);
267            } else {
268                remaining.push_back(item);
269            }
270        }
271
272        // Put unselected elements back
273        *current_input = remaining;
274
275        // Always permute selected elements (Fisher-Yates) to explore all orderings.
276        // Even if commutativity is claimed via manual_proof!, the simulator is
277        // conservative and does not trust it — it still explores permutations.
278        {
279            let slen = selected.len();
280            for i in (1..slen).rev() {
281                let j = (0..=i).generate(driver).unwrap();
282                selected.swap(i, j);
283            }
284        }
285
286        self.to_release = Some(selected);
287    }
288}
289
290/// Scripting a top-level fold releases exactly **one named element per decision**
291/// (like a top-level `assume_ordering`), so intermediate fold states become observable
292/// exactly at the script's release points. The autonomous subset-and-permute path is
293/// never used: it is only sound when the fuzzer explores every subset split.
294impl<T> ScriptableHook for TopLevelFoldHook<T>
295where
296    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
297{
298    type Decision = TopLevelOrderingDecision<T>;
299    type Status = OrderingStatus;
300
301    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
302        let TopLevelOrderingDecision::Next(expected) = decision;
303        Ok(self.input.borrow().iter().any(|item| item == expected))
304    }
305
306    fn apply(&mut self, decision: Self::Decision) {
307        let TopLevelOrderingDecision::Next(expected) = decision;
308        let mut input = self.input.borrow_mut();
309        let index = input.iter().position(|item| item == &expected).unwrap();
310        self.to_release = Some(vec![input.remove(index).unwrap()]);
311    }
312
313    fn implicit(&mut self) {
314        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
315        // hooks; a top-level observation consists of exactly this hook, so it can never
316        // be forced to run without a scripted decision.
317        abort!("implicit decision invoked on a top-level fold hook");
318    }
319
320    fn status(&self) -> Self::Status {
321        OrderingStatus {
322            buffered: self.input.borrow().len(),
323        }
324    }
325
326    fn describe_pending(&self) -> Option<String> {
327        let input = self.input.borrow();
328        (!input.is_empty()).then(|| {
329            format!(
330                "{} buffered fold input(s): {:?}",
331                input.len(),
332                TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
333            )
334        })
335    }
336}
337
338impl<T> ScriptableObservationHook for TopLevelFoldHook<T> where
339    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq
340{
341}
342
343/// Keyed variant of [`TopLevelStreamOrderHook`]. Same one-at-a-time release
344/// strategy to simulate causal interleavings -- see the comment on
345/// [`TopLevelStreamOrderHook`] for the full explanation.
346pub struct TopLevelKeyedStreamOrderHook<K: Hash + Eq + Clone, V> {
347    pub input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
348    pub to_release: Option<Vec<(K, V)>>,
349    pub output: Sender<(K, V)>,
350    pub location: HookLocationMeta,
351    pub format_item_debug: fn(&(K, V)) -> Option<String>,
352}
353
354impl<K: Hash + Eq + Clone, V> RuntimeHook for TopLevelKeyedStreamOrderHook<K, V> {
355    fn has_pending_input(&self) -> bool {
356        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
357        !self.input.borrow().values().all(|q| q.is_empty())
358    }
359
360    fn only_one_possible_decision(&self) -> bool {
361        // A sole buffered element (across all keys) has exactly one possible release.
362        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
363        let total: usize = self.input.borrow().values().map(|q| q.len()).sum();
364        total <= 1
365    }
366
367    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
368        if let Some(to_release) = self.to_release.take() {
369            if !to_release.is_empty()
370                && let Some(log_writer) = log_writer
371            {
372                let HookLocationMeta {
373                    location: batch_location,
374                    line,
375                    caret_indent,
376                } = self.location;
377                let note_str = format!(
378                    "^ observed non-deterministic order: {:?}",
379                    TruncatedVecDebug(
380                        RefCell::new(Some(to_release.iter())),
381                        8,
382                        self.format_item_debug
383                    )
384                );
385
386                let _ = writeln!(log_writer);
387                log_release(
388                    log_writer,
389                    batch_location,
390                    line,
391                    caret_indent,
392                    &note_str,
393                    colored::Color::Green,
394                );
395            }
396
397            for item in to_release {
398                self.output.try_send(item).unwrap();
399            }
400        } else {
401            panic!("No decision to release");
402        }
403    }
404
405    fn location_meta(&self) -> HookLocationMeta {
406        self.location
407    }
408}
409
410impl<K: Hash + Eq + Clone, V> ObservationHook for TopLevelKeyedStreamOrderHook<K, V> {
411    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
412        let mut current_input = self.input.borrow_mut();
413
414        // Collect non-empty keys with their queue lengths
415        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
416        let nonempty_keys: Vec<(K, usize)> = current_input
417            .iter()
418            .filter(|(_, q)| !q.is_empty())
419            .map(|(k, q)| (k.clone(), q.len()))
420            .collect();
421
422        // Pick which key to release from
423        let key_idx = (0..nonempty_keys.len()).generate(driver).unwrap();
424        let (key, queue_len) = &nonempty_keys[key_idx];
425
426        // Pick which item from that key's queue
427        let item_idx = (0..*queue_len).generate(driver).unwrap();
428        let item = current_input
429            .get_mut(key)
430            .unwrap()
431            .remove(item_idx)
432            .unwrap();
433
434        self.to_release = Some(vec![(key.clone(), item)]);
435    }
436}
437
438/// Keyed variant of [`TopLevelStreamOrderHook`]'s scripting: a top-level keyed
439/// `assume_ordering` decision names one `(key, value)` entry to release; values within a
440/// key may be matched at any buffered position (the input is unordered within each key).
441impl<K, V> ScriptableHook for TopLevelKeyedStreamOrderHook<K, V>
442where
443    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
444    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
445{
446    type Decision = TopLevelOrderingDecision<(K, V)>;
447    type Status = OrderingStatus;
448
449    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
450        let TopLevelOrderingDecision::Next((key, expected)) = decision;
451        Ok(self
452            .input
453            .borrow()
454            .get(key)
455            .is_some_and(|queue| queue.iter().any(|item| item == expected)))
456    }
457
458    fn apply(&mut self, decision: Self::Decision) {
459        let TopLevelOrderingDecision::Next((key, expected)) = decision;
460        let mut input = self.input.borrow_mut();
461        let queue = input.get_mut(&key).unwrap();
462        let index = queue.iter().position(|item| item == &expected).unwrap();
463        let item = queue.remove(index).unwrap();
464        self.to_release = Some(vec![(key, item)]);
465    }
466
467    fn implicit(&mut self) {
468        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
469        // hooks; a top-level observation consists of exactly this hook, so it can never
470        // be forced to run without a scripted decision.
471        abort!("implicit decision invoked on a top-level keyed ordering hook");
472    }
473
474    fn status(&self) -> Self::Status {
475        OrderingStatus {
476            buffered: keyed_buffer_len(&self.input.borrow()),
477        }
478    }
479
480    fn describe_pending(&self) -> Option<String> {
481        describe_keyed_pending(&self.input.borrow())
482    }
483}
484
485impl<K, V> ScriptableObservationHook for TopLevelKeyedStreamOrderHook<K, V>
486where
487    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
488    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
489{
490}
491
492/// Top-level variant of [`PartiallyOrderedStreamHook`](super::PartiallyOrderedStreamHook). Same one-at-a-time release
493/// strategy as [`TopLevelKeyedStreamOrderHook`], but always takes from the FRONT
494/// of the chosen key's queue to preserve within-key order.
495pub struct TopLevelPartiallyOrderedStreamHook<K: Hash + Eq + Clone, V> {
496    pub input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
497    pub to_release: Option<Vec<(K, V)>>,
498    pub output: Sender<(K, V)>,
499    pub location: HookLocationMeta,
500    pub format_item_debug: fn(&(K, V)) -> Option<String>,
501}
502
503impl<K: Hash + Eq + Clone, V> RuntimeHook for TopLevelPartiallyOrderedStreamHook<K, V> {
504    fn has_pending_input(&self) -> bool {
505        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
506        !self.input.borrow().values().all(|q| q.is_empty())
507    }
508
509    fn only_one_possible_decision(&self) -> bool {
510        // Within a key the front element is forced, so the only choice is which key
511        // releases next: a single non-empty key is fully forced regardless of depth.
512        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
513        let nonempty_keys = self
514            .input
515            .borrow()
516            .values()
517            .filter(|q| !q.is_empty())
518            .count();
519        nonempty_keys <= 1
520    }
521
522    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
523        if let Some(to_release) = self.to_release.take() {
524            if !to_release.is_empty()
525                && let Some(log_writer) = log_writer
526            {
527                let HookLocationMeta {
528                    location: batch_location,
529                    line,
530                    caret_indent,
531                } = self.location;
532                let note_str = format!(
533                    "^ observed partially-ordered interleaving: {:?}",
534                    TruncatedVecDebug(
535                        RefCell::new(Some(to_release.iter())),
536                        8,
537                        self.format_item_debug
538                    )
539                );
540
541                let _ = writeln!(log_writer);
542                log_release(
543                    log_writer,
544                    batch_location,
545                    line,
546                    caret_indent,
547                    &note_str,
548                    colored::Color::Green,
549                );
550            }
551
552            for item in to_release {
553                self.output.try_send(item).unwrap();
554            }
555        } else {
556            panic!("No decision to release");
557        }
558    }
559
560    fn location_meta(&self) -> HookLocationMeta {
561        self.location
562    }
563}
564
565impl<K: Hash + Eq + Clone, V> ObservationHook for TopLevelPartiallyOrderedStreamHook<K, V> {
566    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
567        let mut current_input = self.input.borrow_mut();
568
569        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
570        let nonempty_keys: Vec<K> = current_input
571            .iter()
572            .filter(|(_, q)| !q.is_empty())
573            .map(|(k, _)| k.clone())
574            .collect();
575
576        // Pick which key to release from
577        let key_idx = (0..nonempty_keys.len()).generate(driver).unwrap();
578        let key = &nonempty_keys[key_idx];
579
580        // Always take from the front to preserve within-key order
581        let item = current_input.get_mut(key).unwrap().pop_front().unwrap();
582
583        self.to_release = Some(vec![(key.clone(), item)]);
584    }
585}
586
587/// Scripting for a top-level `entries_partially_ordered`: each decision names one
588/// `(key, value)` entry to release, and the value must be the *front* of that key's
589/// buffered queue since within-key order is preserved.
590impl<K, V> ScriptableHook for TopLevelPartiallyOrderedStreamHook<K, V>
591where
592    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
593    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
594{
595    type Decision = TopLevelOrderingDecision<(K, V)>;
596    type Status = OrderingStatus;
597
598    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
599        let TopLevelOrderingDecision::Next((key, expected)) = decision;
600        match self.input.borrow().get(key).and_then(VecDeque::front) {
601            Some(front) if front == expected => Ok(true),
602            // Within-key order is preserved, so a mismatching front can never be
603            // released ahead of the named value: the decision is permanently stuck.
604            Some(_) => Err(
605                "next: the named value did not match the front of its key's buffered queue \
606                 (within-key order is preserved by this operator)"
607                    .to_owned(),
608            ),
609            None => Ok(false),
610        }
611    }
612
613    fn apply(&mut self, decision: Self::Decision) {
614        let TopLevelOrderingDecision::Next((key, _expected)) = decision;
615        let mut input = self.input.borrow_mut();
616        let item = input.get_mut(&key).unwrap().pop_front().unwrap();
617        self.to_release = Some(vec![(key, item)]);
618    }
619
620    fn implicit(&mut self) {
621        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
622        // hooks; a top-level observation consists of exactly this hook, so it can never
623        // be forced to run without a scripted decision.
624        abort!("implicit decision invoked on a top-level partially-ordered hook");
625    }
626
627    fn status(&self) -> Self::Status {
628        OrderingStatus {
629            buffered: keyed_buffer_len(&self.input.borrow()),
630        }
631    }
632
633    fn describe_pending(&self) -> Option<String> {
634        describe_keyed_pending(&self.input.borrow())
635    }
636}
637
638impl<K, V> ScriptableObservationHook for TopLevelPartiallyOrderedStreamHook<K, V>
639where
640    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
641    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
642{
643}
644
645/// Top-level merge-ordered hook. Releases one element at a time, picking from
646/// the front of either the first or second input queue. This preserves per-input
647/// order while allowing feedback cycles to deliver elements between releases.
648pub struct TopLevelMergeOrderedHook<T> {
649    pub first: Rc<RefCell<VecDeque<T>>>,
650    pub second: Rc<RefCell<VecDeque<T>>>,
651    pub to_release: Option<Vec<T>>,
652    pub release_source: Option<&'static str>,
653    pub output: Sender<T>,
654    pub location: HookLocationMeta,
655    pub format_item_debug: fn(&T) -> Option<String>,
656}
657
658impl<T> RuntimeHook for TopLevelMergeOrderedHook<T> {
659    fn has_pending_input(&self) -> bool {
660        !self.first.borrow().is_empty() || !self.second.borrow().is_empty()
661    }
662
663    fn only_one_possible_decision(&self) -> bool {
664        // Each side's front element is forced, so the only choice is which side
665        // releases next: it exists only when both sides are non-empty.
666        self.first.borrow().is_empty() || self.second.borrow().is_empty()
667    }
668
669    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
670        if let Some(to_release) = self.to_release.take() {
671            let source = self.release_source.take();
672            if !to_release.is_empty()
673                && let Some(log_writer) = log_writer
674            {
675                let HookLocationMeta {
676                    location: batch_location,
677                    line,
678                    caret_indent,
679                } = self.location;
680                let source_label = source.unwrap_or("?");
681
682                let labeled_iter = to_release.iter().map(|item| (source_label, item));
683
684                let note_str = format!(
685                    "^ observed non-deterministic merge order: {:?}",
686                    TruncatedLabeledVecDebug(
687                        RefCell::new(Some(labeled_iter)),
688                        8,
689                        self.format_item_debug
690                    )
691                );
692
693                let _ = writeln!(log_writer);
694                log_release(
695                    log_writer,
696                    batch_location,
697                    line,
698                    caret_indent,
699                    &note_str,
700                    colored::Color::Green,
701                );
702            }
703
704            for item in to_release {
705                self.output.try_send(item).unwrap();
706            }
707        } else {
708            panic!("No decision to release");
709        }
710    }
711
712    fn location_meta(&self) -> HookLocationMeta {
713        self.location
714    }
715}
716
717impl<T> ObservationHook for TopLevelMergeOrderedHook<T> {
718    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
719        let first_empty = self.first.borrow().is_empty();
720        let second_empty = self.second.borrow().is_empty();
721
722        let (item, source) = if first_empty {
723            (self.second.borrow_mut().pop_front().unwrap(), "r")
724        } else if second_empty {
725            (self.first.borrow_mut().pop_front().unwrap(), "l")
726        } else {
727            let take_second: bool = produce().generate(driver).unwrap();
728            if take_second {
729                (self.second.borrow_mut().pop_front().unwrap(), "r")
730            } else {
731                (self.first.borrow_mut().pop_front().unwrap(), "l")
732            }
733        };
734
735        self.to_release = Some(vec![item]);
736        self.release_source = Some(source);
737    }
738}
739
740/// A scripted decision for a top-level `merge_ordered` observation: which input the next
741/// released element comes from. The element may be named ([`Self::First`] /
742/// [`Self::Second`], asserting it equals the front of that input's buffer) or released
743/// positionally ([`Self::FirstNext`] / [`Self::SecondNext`], releasing the front without
744/// asserting its value). `S` selects *which* front for keyed merges (the key), and is `()`
745/// for plain streams.
746#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
747pub enum MergeDecision<T, S = ()> {
748    /// Release the front of the first (left) input's buffer, which must equal this value.
749    First(T),
750    /// Release the front of the second (right) input's buffer, which must equal this value.
751    Second(T),
752    /// Release the front of the first (left) input's buffer, whatever it is.
753    FirstNext(S),
754    /// Release the front of the second (right) input's buffer, whatever it is.
755    SecondNext(S),
756}
757
758impl<T, S> ScriptDecision for MergeDecision<T, S>
759where
760    T: serde::Serialize + serde::de::DeserializeOwned,
761    S: serde::Serialize + serde::de::DeserializeOwned,
762{
763    fn describe(&self) -> String {
764        match self {
765            MergeDecision::First(_) => "next_first(value)".to_owned(),
766            MergeDecision::Second(_) => "next_second(value)".to_owned(),
767            MergeDecision::FirstNext(_) => "advance_first(..)".to_owned(),
768            MergeDecision::SecondNext(_) => "advance_second(..)".to_owned(),
769        }
770    }
771}
772
773/// The pending-input view a top-level merge hook reports to its test-side handle (see
774/// [`ScriptableHook::status`]), used by `pause_until_*` predicates.
775#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
776pub struct MergeStatus {
777    /// The number of elements buffered from the first (left) input.
778    pub first_buffered: usize,
779    /// The number of elements buffered from the second (right) input.
780    pub second_buffered: usize,
781}
782
783impl<T> ScriptableHook for TopLevelMergeOrderedHook<T>
784where
785    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
786{
787    type Decision = MergeDecision<T>;
788    type Status = MergeStatus;
789
790    fn is_honorable(&self, decision: &MergeDecision<T>) -> Result<bool, String> {
791        let (buffer, expected) = match decision {
792            MergeDecision::First(expected) => (&self.first, Some(expected)),
793            MergeDecision::Second(expected) => (&self.second, Some(expected)),
794            MergeDecision::FirstNext(()) => (&self.first, None),
795            MergeDecision::SecondNext(()) => (&self.second, None),
796        };
797        match (buffer.borrow().front(), expected) {
798            (Some(_), None) => Ok(true),
799            (Some(front), Some(expected)) if front == expected => Ok(true),
800            // Per-input order is preserved, so a mismatching front can never be released
801            // ahead of the named value: the decision is permanently stuck.
802            (Some(_), Some(_)) => Err(
803                "next_first/next_second: the named value did not match the front of that \
804                 input's buffer (per-input order is preserved by merge_ordered)"
805                    .to_owned(),
806            ),
807            (None, _) => Ok(false),
808        }
809    }
810
811    fn apply(&mut self, decision: MergeDecision<T>) {
812        let (item, source) = match decision {
813            MergeDecision::First(_) | MergeDecision::FirstNext(()) => {
814                (self.first.borrow_mut().pop_front().unwrap(), "l")
815            }
816            MergeDecision::Second(_) | MergeDecision::SecondNext(()) => {
817                (self.second.borrow_mut().pop_front().unwrap(), "r")
818            }
819        };
820        self.to_release = Some(vec![item]);
821        self.release_source = Some(source);
822    }
823
824    fn implicit(&mut self) {
825        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
826        // hooks; a top-level observation consists of exactly this hook, so it can never
827        // be forced to run without a scripted decision.
828        abort!("implicit decision invoked on a top-level merge-ordered hook");
829    }
830
831    fn status(&self) -> MergeStatus {
832        MergeStatus {
833            first_buffered: self.first.borrow().len(),
834            second_buffered: self.second.borrow().len(),
835        }
836    }
837
838    fn describe_pending(&self) -> Option<String> {
839        let first = self.first.borrow();
840        let second = self.second.borrow();
841        (!first.is_empty() || !second.is_empty()).then(|| {
842            format!(
843                "{} + {} buffered item(s) (first + second input)",
844                first.len(),
845                second.len()
846            )
847        })
848    }
849}
850
851impl<T> ScriptableObservationHook for TopLevelMergeOrderedHook<T> where
852    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq
853{
854}
855
856/// Keyed variant of [`TopLevelMergeOrderedHook`]. Releases one element at a
857/// time, picking from the front of some key's queue in either the first or the
858/// second input. This preserves per-input order within each key while allowing
859/// arbitrary interleaving both across the two inputs and across keys (which is
860/// unconstrained for keyed streams), and lets feedback cycles deliver elements
861/// between releases.
862pub struct TopLevelKeyedMergeOrderedHook<K: Hash + Eq + Clone, V> {
863    pub first: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
864    pub second: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
865    pub to_release: Option<Vec<(K, V)>>,
866    pub release_source: Option<&'static str>,
867    pub output: Sender<(K, V)>,
868    pub location: HookLocationMeta,
869    pub format_item_debug: fn(&(K, V)) -> Option<String>,
870}
871
872impl<K: Hash + Eq + Clone, V> RuntimeHook for TopLevelKeyedMergeOrderedHook<K, V> {
873    fn has_pending_input(&self) -> bool {
874        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
875        let first_nonempty = !self.first.borrow().values().all(|q| q.is_empty());
876        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
877        let second_nonempty = !self.second.borrow().values().all(|q| q.is_empty());
878        first_nonempty || second_nonempty
879    }
880
881    fn only_one_possible_decision(&self) -> bool {
882        // Each (side, key) queue's front element is forced, so the only choice is which
883        // queue releases next.
884        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
885        let first_count = self
886            .first
887            .borrow()
888            .values()
889            .filter(|q| !q.is_empty())
890            .count();
891        #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
892        let second_count = self
893            .second
894            .borrow()
895            .values()
896            .filter(|q| !q.is_empty())
897            .count();
898        first_count + second_count <= 1
899    }
900
901    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
902        if let Some(to_release) = self.to_release.take() {
903            let source = self.release_source.take();
904            if !to_release.is_empty()
905                && let Some(log_writer) = log_writer
906            {
907                let HookLocationMeta {
908                    location: batch_location,
909                    line,
910                    caret_indent,
911                } = self.location;
912                let source_label = source.unwrap_or("?");
913
914                let labeled_iter = to_release.iter().map(|item| (source_label, item));
915
916                let note_str = format!(
917                    "^ observed non-deterministic merge order: {:?}",
918                    TruncatedLabeledVecDebug(
919                        RefCell::new(Some(labeled_iter)),
920                        8,
921                        self.format_item_debug
922                    )
923                );
924
925                let _ = writeln!(log_writer);
926                log_release(
927                    log_writer,
928                    batch_location,
929                    line,
930                    caret_indent,
931                    &note_str,
932                    colored::Color::Green,
933                );
934            }
935
936            for item in to_release {
937                self.output.try_send(item).unwrap();
938            }
939        } else {
940            panic!("No decision to release");
941        }
942    }
943
944    fn location_meta(&self) -> HookLocationMeta {
945        self.location
946    }
947}
948
949impl<K: Hash + Eq + Clone, V> ObservationHook for TopLevelKeyedMergeOrderedHook<K, V> {
950    fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>) {
951        // Collect candidates: for each non-empty key queue in either input, we
952        // can release its front element. `false` = first input, `true` = second.
953        let mut candidates: Vec<(bool, K)> = Vec::new();
954        {
955            #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
956            for (k, q) in self.first.borrow().iter() {
957                if !q.is_empty() {
958                    candidates.push((false, k.clone()));
959                }
960            }
961            #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
962            for (k, q) in self.second.borrow().iter() {
963                if !q.is_empty() {
964                    candidates.push((true, k.clone()));
965                }
966            }
967        }
968
969        let idx = (0..candidates.len()).generate(driver).unwrap();
970        let (take_second, key) = &candidates[idx];
971        let take_second = *take_second;
972
973        let item = if take_second {
974            self.second
975                .borrow_mut()
976                .get_mut(key)
977                .unwrap()
978                .pop_front()
979                .unwrap()
980        } else {
981            self.first
982                .borrow_mut()
983                .get_mut(key)
984                .unwrap()
985                .pop_front()
986                .unwrap()
987        };
988
989        self.to_release = Some(vec![(key.clone(), item)]);
990        self.release_source = Some(if take_second { "r" } else { "l" });
991    }
992}
993
994impl<K, V> ScriptableHook for TopLevelKeyedMergeOrderedHook<K, V>
995where
996    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
997    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
998{
999    type Decision = MergeDecision<(K, V), K>;
1000    type Status = MergeStatus;
1001
1002    fn is_honorable(&self, decision: &MergeDecision<(K, V), K>) -> Result<bool, String> {
1003        let (buffer, key, expected) = match decision {
1004            MergeDecision::First((key, expected)) => (&self.first, key, Some(expected)),
1005            MergeDecision::Second((key, expected)) => (&self.second, key, Some(expected)),
1006            MergeDecision::FirstNext(key) => (&self.first, key, None),
1007            MergeDecision::SecondNext(key) => (&self.second, key, None),
1008        };
1009        match (buffer.borrow().get(key).and_then(VecDeque::front), expected) {
1010            (Some(_), None) => Ok(true),
1011            (Some(front), Some(expected)) if front == expected => Ok(true),
1012            // Per-input order within a key is preserved, so a mismatching front can
1013            // never be released ahead of the named value: the decision is permanently
1014            // stuck.
1015            (Some(_), Some(_)) => Err(
1016                "next_first/next_second: the named value did not match the front of its \
1017                 key's buffer in that input (per-input within-key order is preserved by \
1018                 merge_ordered)"
1019                    .to_owned(),
1020            ),
1021            (None, _) => Ok(false),
1022        }
1023    }
1024
1025    fn apply(&mut self, decision: MergeDecision<(K, V), K>) {
1026        let (buffer, key, source) = match decision {
1027            MergeDecision::First((key, _)) => (&self.first, key, "l"),
1028            MergeDecision::Second((key, _)) => (&self.second, key, "r"),
1029            MergeDecision::FirstNext(key) => (&self.first, key, "l"),
1030            MergeDecision::SecondNext(key) => (&self.second, key, "r"),
1031        };
1032        let item = buffer
1033            .borrow_mut()
1034            .get_mut(&key)
1035            .unwrap()
1036            .pop_front()
1037            .unwrap();
1038        self.to_release = Some(vec![(key, item)]);
1039        self.release_source = Some(source);
1040    }
1041
1042    fn implicit(&mut self) {
1043        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
1044        // hooks; a top-level observation consists of exactly this hook, so it can never
1045        // be forced to run without a scripted decision.
1046        abort!("implicit decision invoked on a top-level keyed merge-ordered hook");
1047    }
1048
1049    fn status(&self) -> MergeStatus {
1050        MergeStatus {
1051            first_buffered: keyed_buffer_len(&self.first.borrow()),
1052            second_buffered: keyed_buffer_len(&self.second.borrow()),
1053        }
1054    }
1055
1056    fn describe_pending(&self) -> Option<String> {
1057        let first = keyed_buffer_len(&self.first.borrow());
1058        let second = keyed_buffer_len(&self.second.borrow());
1059        (first + second > 0).then(|| {
1060            format!(
1061                "{} + {} buffered item(s) (first + second input)",
1062                first, second
1063            )
1064        })
1065    }
1066}
1067
1068impl<K, V> ScriptableObservationHook for TopLevelKeyedMergeOrderedHook<K, V>
1069where
1070    K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
1071    V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
1072{
1073}
1074
1075/// Top-level hook for `assume_retries` (`AtLeastOnce` → `ExactlyOnce`) on a **`NoOrder`**
1076/// stream: the point where the simulator injects the retries the type says downstream
1077/// must tolerate. Each `release(value, copies)` decision picks one buffered element and
1078/// a copy count `copies >= 1`, releasing that many copies and consuming the element.
1079///
1080/// The copy count is the *entire* decision: the output stays `NoOrder`, so the placement
1081/// of the copies is unobservable — downstream ordering points shuffle them like any
1082/// other elements. Releases stay one element per decision because timing matters on an
1083/// unordered stream: a released element can cycle back through feedback and be chosen
1084/// ahead of still-buffered input. This is what makes `assume_retries().assume_ordering()`
1085/// explore the same state space as `assume_ordering().assume_retries()` (see
1086/// [`TopLevelAtLeastOnceOrderHook`] for the other factoring): here each instance's
1087/// copies become distinct elements of a `NoOrder + ExactlyOnce` multiset, and the
1088/// downstream ordering observation arranges that multiset arbitrarily.
1089///
1090/// There is no autonomous ([`ObservationHook`]) implementation: copy counts make the
1091/// decision space infinite, so this hook exists only bound to a sim hook handle.
1092pub struct TopLevelNoOrderStreamRetriesHook<T> {
1093    pub input: Rc<RefCell<VecDeque<T>>>,
1094    pub to_release: Option<Vec<T>>,
1095    pub output: Sender<T>,
1096    pub location: HookLocationMeta,
1097    pub format_item_debug: fn(&T) -> Option<String>,
1098}
1099
1100/// Top-level hook for `assume_retries` (`AtLeastOnce` → `ExactlyOnce`) on a
1101/// **`TotalOrder`** stream. A decision supplies the released sequence for a **prefix**
1102/// of the buffered queue: the prefix's values in order, where each value may be
1103/// repeated back-to-back to simulate retries.
1104///
1105/// Unlike the unordered kinds, there is no one-element-at-a-time protocol here: on an
1106/// ordered stream a feedback-cycled element always queues *behind* everything pending
1107/// (it cannot race ahead of buffered input), and downstream tick-input hooks choose
1108/// their own batch boundaries regardless of arrival granularity, so releasing several
1109/// elements in one observation forgoes no reachable behavior. A decision fires only
1110/// once it is honorable *in full* against the buffered queue.
1111///
1112/// Finality is explicit, exactly as for [`TopLevelAtLeastOnceOrderHook`]'s
1113/// `emit`/`emit_final`: elements *inside* the sequence are finalized by the sequence
1114/// itself (a later value can only start once the earlier one's copies ended — order is
1115/// preserved), but the **trailing** element's fate is the script's declaration.
1116/// `release(values)` keeps it queued (`front_started`) so later decisions may release
1117/// more copies of it, while `release_final(values)` consumes it — and, as always, any
1118/// queued element leaves the hook subject to the forgotten-hook check until a decision
1119/// or pause accounts for it. When equal elements sit next to each other in the queue,
1120/// scripted repeats are attributed to as many distinct queue elements as possible
1121/// (equal elements are indistinguishable to the script, so this is unobservable).
1122///
1123/// Back-to-back-only expansion is deliberate. An `AtLeastOnce` collection `[A, B]`
1124/// denotes the physical realizations `A⁺B⁺` (each element instance stutters *in
1125/// place*); a delayed redelivery like `ABAB` is a *different* collection
1126/// (`[A, B, A, B]`), mintable only where the *order* dimension is observed —
1127/// `assume_ordering` on an `AtLeastOnce` stream ([`TopLevelAtLeastOnceOrderHook`]) can
1128/// emit an instance into several non-adjacent slots, which then arrive here as separate
1129/// slots to expand. This factoring makes `assume_ordering().assume_retries()` and
1130/// `assume_retries().assume_ordering()` explore the same state space: any sequence in
1131/// which every input instance appears at least once.
1132///
1133/// There is no autonomous ([`ObservationHook`]) implementation: cardinalities make the
1134/// decision space infinite, so this hook exists only bound to a sim hook handle.
1135pub struct TopLevelOrderedStreamRetriesHook<T> {
1136    pub input: Rc<RefCell<VecDeque<T>>>,
1137    pub to_release: Option<Vec<T>>,
1138    /// Whether the staged release is non-final (the trailing matched element stays
1139    /// queued), noted in the release log.
1140    pub release_provisional: bool,
1141    /// Whether the front element's copies were started by an earlier non-final
1142    /// `release`: the next decision may repeat it without re-starting it, and a
1143    /// `release_final` with no values may consume it as-is.
1144    pub front_started: bool,
1145    pub output: Sender<T>,
1146    pub location: HookLocationMeta,
1147    pub format_item_debug: fn(&T) -> Option<String>,
1148}
1149
1150impl<T> RuntimeHook for TopLevelNoOrderStreamRetriesHook<T> {
1151    fn has_pending_input(&self) -> bool {
1152        !self.input.borrow().is_empty()
1153    }
1154
1155    fn only_one_possible_decision(&self) -> bool {
1156        // Even a sole buffered element admits infinitely many decisions (its
1157        // cardinality), so only an empty buffer is unique.
1158        self.input.borrow().is_empty()
1159    }
1160
1161    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1162        if let Some(to_release) = self.to_release.take() {
1163            if !to_release.is_empty()
1164                && let Some(log_writer) = log_writer
1165            {
1166                let HookLocationMeta {
1167                    location: batch_location,
1168                    line,
1169                    caret_indent,
1170                } = self.location;
1171                let note_str = format!(
1172                    "^ observed non-deterministic retries: {:?}",
1173                    TruncatedVecDebug(
1174                        RefCell::new(Some(to_release.iter())),
1175                        8,
1176                        self.format_item_debug
1177                    )
1178                );
1179
1180                let _ = writeln!(log_writer);
1181                log_release(
1182                    log_writer,
1183                    batch_location,
1184                    line,
1185                    caret_indent,
1186                    &note_str,
1187                    colored::Color::Green,
1188                );
1189            }
1190
1191            for item in to_release {
1192                self.output.try_send(item).unwrap();
1193            }
1194        } else {
1195            panic!("No decision to release");
1196        }
1197    }
1198
1199    fn location_meta(&self) -> HookLocationMeta {
1200        self.location
1201    }
1202}
1203
1204impl<T> RuntimeHook for TopLevelOrderedStreamRetriesHook<T> {
1205    fn has_pending_input(&self) -> bool {
1206        !self.input.borrow().is_empty()
1207    }
1208
1209    fn only_one_possible_decision(&self) -> bool {
1210        // Even a sole buffered element admits infinitely many decisions (its
1211        // cardinality), so only an empty buffer is unique.
1212        self.input.borrow().is_empty()
1213    }
1214
1215    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1216        if let Some(to_release) = self.to_release.take() {
1217            let provisional = self.release_provisional;
1218            if !to_release.is_empty()
1219                && let Some(log_writer) = log_writer
1220            {
1221                let HookLocationMeta {
1222                    location: batch_location,
1223                    line,
1224                    caret_indent,
1225                } = self.location;
1226                let note_str = format!(
1227                    "^ observed non-deterministic retries: {:?}{}",
1228                    TruncatedVecDebug(
1229                        RefCell::new(Some(to_release.iter())),
1230                        8,
1231                        self.format_item_debug
1232                    ),
1233                    if provisional {
1234                        " (last value may repeat)"
1235                    } else {
1236                        ""
1237                    }
1238                );
1239
1240                let _ = writeln!(log_writer);
1241                log_release(
1242                    log_writer,
1243                    batch_location,
1244                    line,
1245                    caret_indent,
1246                    &note_str,
1247                    colored::Color::Green,
1248                );
1249            }
1250
1251            for item in to_release {
1252                self.output.try_send(item).unwrap();
1253            }
1254        } else {
1255            panic!("No decision to release");
1256        }
1257    }
1258
1259    fn location_meta(&self) -> HookLocationMeta {
1260        self.location
1261    }
1262}
1263
1264/// A scripted decision for a top-level `assume_retries` observation on a **`NoOrder`**
1265/// stream: release `copies >= 1` copies of the named buffered element, consuming it.
1266#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1267pub enum NoOrderRetriesDecision<T> {
1268    Release(T, usize),
1269}
1270
1271impl<T> ScriptDecision for NoOrderRetriesDecision<T>
1272where
1273    T: serde::Serialize + serde::de::DeserializeOwned,
1274{
1275    fn describe(&self) -> String {
1276        let NoOrderRetriesDecision::Release(_, copies) = self;
1277        format!("release(value, {})", copies)
1278    }
1279}
1280
1281/// A scripted decision for a top-level `assume_retries` observation on a
1282/// **`TotalOrder`** stream: the released sequence for a prefix of the buffered queue,
1283/// with finality of the trailing element declared explicitly (see
1284/// [`TopLevelOrderedStreamRetriesHook`]).
1285#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1286pub enum OrderedRetriesDecision<T> {
1287    /// Release this sequence, keeping the trailing matched element queued: later
1288    /// decisions may release more copies of it before it is finalized.
1289    Release(Vec<T>),
1290    /// Release this sequence and consume the trailing matched element (or, with no
1291    /// values, consume a front element whose copies were all released by earlier
1292    /// decisions).
1293    ReleaseFinal(Vec<T>),
1294    /// Release the given number of copies of the front element, whatever it is, and
1295    /// consume it.
1296    ReleaseNext(usize),
1297}
1298
1299impl<T> ScriptDecision for OrderedRetriesDecision<T>
1300where
1301    T: serde::Serialize + serde::de::DeserializeOwned,
1302{
1303    fn describe(&self) -> String {
1304        match self {
1305            OrderedRetriesDecision::Release(values) => {
1306                format!("release({} value(s))", values.len())
1307            }
1308            OrderedRetriesDecision::ReleaseFinal(values) => {
1309                format!("release_final({} value(s))", values.len())
1310            }
1311            OrderedRetriesDecision::ReleaseNext(copies) => format!("release_next({})", copies),
1312        }
1313    }
1314}
1315
1316/// Builds the staged release for an honored `release`: `copies - 1` clones plus the
1317/// consumed element itself.
1318fn expand_retries<T: Clone>(item: T, copies: usize) -> Vec<T> {
1319    let mut to_release = Vec::with_capacity(copies);
1320    for _ in 1..copies {
1321        to_release.push(item.clone());
1322    }
1323    to_release.push(item);
1324    to_release
1325}
1326
1327/// Defensive check mirrored from the handle's call-site assertion: an element observed
1328/// zero times would be a *loss*, which `AtLeastOnce` does not permit.
1329fn check_copies_at_least_one(copies: usize) -> Result<(), String> {
1330    if copies == 0 {
1331        Err("the number of copies must be at least 1 (at-least-once permits duplicates, not losses)".to_owned())
1332    } else {
1333        Ok(())
1334    }
1335}
1336
1337impl<T> ScriptableHook for TopLevelNoOrderStreamRetriesHook<T>
1338where
1339    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq,
1340{
1341    type Decision = NoOrderRetriesDecision<T>;
1342    type Status = OrderingStatus;
1343
1344    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
1345        let NoOrderRetriesDecision::Release(expected, copies) = decision;
1346        check_copies_at_least_one(*copies)?;
1347        Ok(self.input.borrow().iter().any(|item| item == expected))
1348    }
1349
1350    fn apply(&mut self, decision: Self::Decision) {
1351        let NoOrderRetriesDecision::Release(expected, copies) = decision;
1352        let mut input = self.input.borrow_mut();
1353        let index = input.iter().position(|item| item == &expected).unwrap();
1354        let item = input.remove(index).unwrap();
1355        self.to_release = Some(expand_retries(item, copies));
1356    }
1357
1358    fn implicit(&mut self) {
1359        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
1360        // hooks; a top-level observation consists of exactly this hook, so it can never
1361        // be forced to run without a scripted decision.
1362        abort!("implicit decision invoked on a top-level retries hook");
1363    }
1364
1365    fn status(&self) -> Self::Status {
1366        OrderingStatus {
1367            buffered: self.input.borrow().len(),
1368        }
1369    }
1370
1371    fn describe_pending(&self) -> Option<String> {
1372        let input = self.input.borrow();
1373        (!input.is_empty()).then(|| {
1374            format!(
1375                "{} buffered item(s) awaiting a retries decision: {:?}",
1376                input.len(),
1377                TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1378            )
1379        })
1380    }
1381}
1382
1383impl<T> ScriptableObservationHook for TopLevelNoOrderStreamRetriesHook<T> where
1384    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq
1385{
1386}
1387
1388/// The result of matching a scripted release sequence against an ordered retries
1389/// hook's buffered queue (see [`walk_ordered_retries`]).
1390enum PrefixWalk {
1391    /// The whole sequence matched, fully finalizing the first `consumed` queue elements
1392    /// (the trailing matched element, if any, is *not* counted: its fate is the
1393    /// decision's finality flag).
1394    Complete { consumed: usize },
1395    /// The sequence ran past the buffered queue; it may become honorable once more
1396    /// input arrives.
1397    NeedMoreInput,
1398    /// A scripted value contradicts the buffered queue; the decision can never fire
1399    /// (input order is preserved).
1400    Mismatch,
1401}
1402
1403/// Matches `values` against the queue: each value either continues the current element
1404/// (a back-to-back repeat) or starts the next queued element. `front_started` carries
1405/// over from an earlier non-final decision that left the front element's copies open.
1406/// When a value could do either (equal elements adjacent in the queue), it starts the
1407/// next element — repeats are attributed to as many distinct queue elements as
1408/// possible, which is unobservable (the elements are equal) but keeps the accounting
1409/// canonical.
1410fn walk_ordered_retries<T: PartialEq>(
1411    input: &VecDeque<T>,
1412    front_started: bool,
1413    values: &[T],
1414) -> PrefixWalk {
1415    let mut consumed = 0;
1416    let mut started = front_started;
1417    for value in values {
1418        if !started {
1419            match input.get(consumed) {
1420                Some(front) if front == value => started = true,
1421                Some(_) => return PrefixWalk::Mismatch,
1422                None => return PrefixWalk::NeedMoreInput,
1423            }
1424        } else if input.get(consumed + 1).is_some_and(|next| next == value) {
1425            // Starts the next queued element, finalizing the current one.
1426            consumed += 1;
1427        } else if input.get(consumed).is_some_and(|current| current == value) {
1428            // Another back-to-back copy of the current element.
1429        } else if consumed + 1 >= input.len() {
1430            return PrefixWalk::NeedMoreInput;
1431        } else {
1432            return PrefixWalk::Mismatch;
1433        }
1434    }
1435    PrefixWalk::Complete { consumed }
1436}
1437
1438impl<T> ScriptableHook for TopLevelOrderedStreamRetriesHook<T>
1439where
1440    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq,
1441{
1442    type Decision = OrderedRetriesDecision<T>;
1443    type Status = OrderingStatus;
1444
1445    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
1446        let values = match decision {
1447            OrderedRetriesDecision::Release(values) => {
1448                if values.is_empty() {
1449                    // The handle asserts this at the scripting call site; kept as a
1450                    // defensive permanent error.
1451                    return Err(
1452                        "a non-final release must contain at least one value (declare \
1453                         intentional buffering with the pause family instead)"
1454                            .to_owned(),
1455                    );
1456                }
1457                values
1458            }
1459            OrderedRetriesDecision::ReleaseFinal(values) => {
1460                if values.is_empty() {
1461                    return if self.front_started {
1462                        Ok(true)
1463                    } else {
1464                        Err(
1465                            "release_final with no values requires an earlier release to \
1466                             have left the front element's copies open"
1467                                .to_owned(),
1468                        )
1469                    };
1470                }
1471                values
1472            }
1473            OrderedRetriesDecision::ReleaseNext(copies) => {
1474                check_copies_at_least_one(*copies)?;
1475                return Ok(!self.input.borrow().is_empty());
1476            }
1477        };
1478        match walk_ordered_retries(&self.input.borrow(), self.front_started, values) {
1479            PrefixWalk::Complete { .. } => Ok(true),
1480            PrefixWalk::NeedMoreInput => Ok(false),
1481            // Input order is preserved, so a contradicting value can never be released:
1482            // the decision is permanently stuck.
1483            PrefixWalk::Mismatch => Err(
1484                "the scripted values did not match the buffered queue (input order is \
1485                 preserved, so values are released in arrival order and repeat only \
1486                 back-to-back)"
1487                    .to_owned(),
1488            ),
1489        }
1490    }
1491
1492    fn apply(&mut self, decision: Self::Decision) {
1493        match decision {
1494            OrderedRetriesDecision::Release(values) => {
1495                let PrefixWalk::Complete { consumed } =
1496                    walk_ordered_retries(&self.input.borrow(), self.front_started, &values)
1497                else {
1498                    abort!("ordered retries decision applied while not honorable");
1499                };
1500                self.input.borrow_mut().drain(..consumed);
1501                self.to_release = Some(values);
1502                self.release_provisional = true;
1503                self.front_started = true;
1504            }
1505            OrderedRetriesDecision::ReleaseFinal(values) => {
1506                if values.is_empty() {
1507                    // Finalizes a front element whose copies were all released by
1508                    // earlier decisions; nothing more is delivered.
1509                    self.input.borrow_mut().pop_front().unwrap();
1510                    self.to_release = Some(vec![]);
1511                } else {
1512                    let PrefixWalk::Complete { consumed } =
1513                        walk_ordered_retries(&self.input.borrow(), self.front_started, &values)
1514                    else {
1515                        abort!("ordered retries decision applied while not honorable");
1516                    };
1517                    // The trailing matched element is finalized too.
1518                    self.input.borrow_mut().drain(..=consumed);
1519                    self.to_release = Some(values);
1520                }
1521                self.release_provisional = false;
1522                self.front_started = false;
1523            }
1524            OrderedRetriesDecision::ReleaseNext(copies) => {
1525                let item = self.input.borrow_mut().pop_front().unwrap();
1526                self.to_release = Some(expand_retries(item, copies));
1527                self.release_provisional = false;
1528                self.front_started = false;
1529            }
1530        }
1531    }
1532
1533    fn implicit(&mut self) {
1534        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
1535        // hooks; a top-level observation consists of exactly this hook, so it can never
1536        // be forced to run without a scripted decision.
1537        abort!("implicit decision invoked on a top-level retries hook");
1538    }
1539
1540    fn status(&self) -> Self::Status {
1541        OrderingStatus {
1542            buffered: self.input.borrow().len(),
1543        }
1544    }
1545
1546    fn describe_pending(&self) -> Option<String> {
1547        let input = self.input.borrow();
1548        (!input.is_empty()).then(|| {
1549            format!(
1550                "{} buffered item(s) awaiting a retries decision{}: {:?}",
1551                input.len(),
1552                if self.front_started {
1553                    " (the front element's copies are still open)"
1554                } else {
1555                    ""
1556                },
1557                TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1558            )
1559        })
1560    }
1561}
1562
1563impl<T> ScriptableObservationHook for TopLevelOrderedStreamRetriesHook<T> where
1564    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq
1565{
1566}
1567
1568/// Top-level hook for `assume_ordering` on an `AtLeastOnce` stream. Ordering a stream
1569/// that may carry retries means choosing the sequence of **slots** each element instance
1570/// occupies, and an instance may occupy several non-adjacent slots: from a queue of
1571/// `{A, B}`, `ABAB…` is a legitimate slot sequence (each extra slot is a delayed
1572/// redelivery, still `AtLeastOnce`). So each decision releases one slot:
1573///
1574/// - `emit(value)` releases a copy and *keeps* the value buffered for future slots;
1575/// - `emit_final(value)` releases the value's last slot and consumes every buffered
1576///   element equal to it, so the queue eventually drains and the forgotten-hook check
1577///   stays meaningful.
1578///
1579/// Adjacent duplicate slots are rejected as redundant when only one buffered element
1580/// matches the value: as collections under `TotalOrder + AtLeastOnce` they are
1581/// equivalent to the single slot (`AA` and `A` both denote `A⁺`), and the *cardinality*
1582/// dimension belongs to the downstream `assume_retries` observation
1583/// ([`TopLevelOrderedStreamRetriesHook`]). Decisions name **values**, not element
1584/// instances (equal instances are indistinguishable to the script), so the hook works
1585/// at the value-class level: `emit(v)` releases one slot for the class, and
1586/// `emit_final(v)` releases its last slot while consuming *every* buffered element
1587/// equal to `v` — checking, via the per-class emission counts, that the class received
1588/// at least as many slots as it has buffered elements (each element must be observed at
1589/// least once; at-least-once permits duplicates, not losses). Two adjacent slots of the
1590/// same value are legitimate exactly when two or more buffered elements share it.
1591///
1592/// There is no autonomous ([`ObservationHook`]) implementation: re-emission makes the
1593/// decision space infinite, so this hook exists only bound to a sim hook handle.
1594pub struct TopLevelAtLeastOnceOrderHook<T> {
1595    pub input: Rc<RefCell<VecDeque<T>>>,
1596    pub to_release: Option<Vec<T>>,
1597    /// Whether the staged release is a non-final slot (its value stays buffered),
1598    /// noted in the release log.
1599    pub release_provisional: bool,
1600    /// The value released by the immediately-preceding decision, if that decision was a
1601    /// provisional `emit` (elements equal to it are still buffered). The next decision
1602    /// may name the same value only if two or more buffered elements share it —
1603    /// otherwise it would place the same sole element in adjacent slots, a redundant
1604    /// duplicate. `None` after an `emit_final` (everything equal to the released value
1605    /// was consumed, so a later equal arrival is a genuinely new element).
1606    pub last_emitted: Option<T>,
1607    /// Per value-class count of provisional slots released so far, consulted by
1608    /// `emit_final` to check that every buffered element of the class got a slot.
1609    /// An entry lives from a class's first `emit` until its `emit_final` consumes it.
1610    pub emitted_counts: Vec<(T, usize)>,
1611    pub output: Sender<T>,
1612    pub location: HookLocationMeta,
1613    pub format_item_debug: fn(&T) -> Option<String>,
1614}
1615
1616impl<T> RuntimeHook for TopLevelAtLeastOnceOrderHook<T> {
1617    fn has_pending_input(&self) -> bool {
1618        !self.input.borrow().is_empty()
1619    }
1620
1621    fn only_one_possible_decision(&self) -> bool {
1622        // Even a sole buffered element admits more than one decision (emit it
1623        // provisionally or finally), so only an empty buffer is unique.
1624        self.input.borrow().is_empty()
1625    }
1626
1627    fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1628        if let Some(to_release) = self.to_release.take() {
1629            let provisional = std::mem::take(&mut self.release_provisional);
1630            if !to_release.is_empty()
1631                && let Some(log_writer) = log_writer
1632            {
1633                let HookLocationMeta {
1634                    location: batch_location,
1635                    line,
1636                    caret_indent,
1637                } = self.location;
1638                let note_str = format!(
1639                    "^ observed non-deterministic order: {:?}{}",
1640                    TruncatedVecDebug(
1641                        RefCell::new(Some(to_release.iter())),
1642                        8,
1643                        self.format_item_debug
1644                    ),
1645                    if provisional { " (may re-emit)" } else { "" }
1646                );
1647
1648                let _ = writeln!(log_writer);
1649                log_release(
1650                    log_writer,
1651                    batch_location,
1652                    line,
1653                    caret_indent,
1654                    &note_str,
1655                    colored::Color::Green,
1656                );
1657            }
1658
1659            for item in to_release {
1660                self.output.try_send(item).unwrap();
1661            }
1662        } else {
1663            panic!("No decision to release");
1664        }
1665    }
1666
1667    fn location_meta(&self) -> HookLocationMeta {
1668        self.location
1669    }
1670}
1671
1672/// A scripted decision for a top-level `assume_ordering` on an `AtLeastOnce` stream:
1673/// release one slot for a buffered element instance, either keeping the instance for
1674/// future re-emission ([`Self::Emit`]) or consuming it ([`Self::EmitFinal`]).
1675#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1676pub enum AtLeastOnceOrderingDecision<T> {
1677    Emit(T),
1678    EmitFinal(T),
1679}
1680
1681impl<T> ScriptDecision for AtLeastOnceOrderingDecision<T>
1682where
1683    T: serde::Serialize + serde::de::DeserializeOwned,
1684{
1685    fn describe(&self) -> String {
1686        match self {
1687            AtLeastOnceOrderingDecision::Emit(_) => "emit(value)".to_owned(),
1688            AtLeastOnceOrderingDecision::EmitFinal(_) => "emit_final(value)".to_owned(),
1689        }
1690    }
1691}
1692
1693impl<T> TopLevelAtLeastOnceOrderHook<T>
1694where
1695    T: PartialEq,
1696{
1697    /// The number of buffered elements equal to `value`.
1698    fn buffered_count(&self, value: &T) -> usize {
1699        self.input
1700            .borrow()
1701            .iter()
1702            .filter(|item| *item == value)
1703            .count()
1704    }
1705
1706    /// The number of provisional slots already released for `value`'s class.
1707    fn emitted_count(&self, value: &T) -> usize {
1708        self.emitted_counts
1709            .iter()
1710            .find(|(v, _)| v == value)
1711            .map_or(0, |(_, count)| *count)
1712    }
1713
1714    /// Validates a decision naming `value` against the value-class rules. Returns
1715    /// `Ok(true)` when honorable now, `Ok(false)` when it should wait for a matching
1716    /// element to arrive, and `Err` for a mis-stated script.
1717    fn check_decision(&self, value: &T, is_final: bool) -> Result<bool, String> {
1718        let buffered = self.buffered_count(value);
1719        if buffered == 0 {
1720            return Ok(false);
1721        }
1722        if self.last_emitted.as_ref() == Some(value) && buffered < 2 {
1723            return Err(
1724                "the same value cannot be released twice in a row when only one buffered \
1725                 element matches it (back-to-back duplicates are the downstream \
1726                 `assume_retries`'s decision)"
1727                    .to_owned(),
1728            );
1729        }
1730        if is_final && self.emitted_count(value) + 1 < buffered {
1731            return Err(format!(
1732                "emit_final consumes all {} buffered element(s) equal to the value, but \
1733                 only {} would have been emitted (every element must be emitted at least \
1734                 once)",
1735                buffered,
1736                self.emitted_count(value) + 1,
1737            ));
1738        }
1739        Ok(true)
1740    }
1741}
1742
1743impl<T> ScriptableHook for TopLevelAtLeastOnceOrderHook<T>
1744where
1745    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq,
1746{
1747    type Decision = AtLeastOnceOrderingDecision<T>;
1748    type Status = OrderingStatus;
1749
1750    fn is_honorable(&self, decision: &Self::Decision) -> Result<bool, String> {
1751        match decision {
1752            AtLeastOnceOrderingDecision::Emit(expected) => self.check_decision(expected, false),
1753            AtLeastOnceOrderingDecision::EmitFinal(expected) => self.check_decision(expected, true),
1754        }
1755    }
1756
1757    fn apply(&mut self, decision: Self::Decision) {
1758        match decision {
1759            AtLeastOnceOrderingDecision::Emit(expected) => {
1760                if let Some((_, count)) =
1761                    self.emitted_counts.iter_mut().find(|(v, _)| *v == expected)
1762                {
1763                    *count += 1;
1764                } else {
1765                    self.emitted_counts.push((expected.clone(), 1));
1766                }
1767                self.to_release = Some(vec![expected.clone()]);
1768                self.release_provisional = true;
1769                self.last_emitted = Some(expected);
1770            }
1771            AtLeastOnceOrderingDecision::EmitFinal(expected) => {
1772                // The last slot of the value's class: release one copy and consume
1773                // every buffered element equal to it (the script has no way to name
1774                // individual instances, so retirement is class-level).
1775                self.input.borrow_mut().retain(|item| item != &expected);
1776                self.emitted_counts.retain(|(v, _)| v != &expected);
1777                self.to_release = Some(vec![expected]);
1778                self.release_provisional = false;
1779                self.last_emitted = None;
1780            }
1781        }
1782    }
1783
1784    fn implicit(&mut self) {
1785        // Implicit behavior exists for tick hooks whose tick is forced to run by *other*
1786        // hooks; a top-level observation consists of exactly this hook, so it can never
1787        // be forced to run without a scripted decision.
1788        abort!("implicit decision invoked on a top-level at-least-once ordering hook");
1789    }
1790
1791    fn status(&self) -> Self::Status {
1792        OrderingStatus {
1793            buffered: self.input.borrow().len(),
1794        }
1795    }
1796
1797    fn describe_pending(&self) -> Option<String> {
1798        let input = self.input.borrow();
1799        (!input.is_empty()).then(|| {
1800            format!(
1801                "{} buffered ordering item(s) (each may be emitted into several slots): {:?}",
1802                input.len(),
1803                TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1804            )
1805        })
1806    }
1807}
1808
1809impl<T> ScriptableObservationHook for TopLevelAtLeastOnceOrderHook<T> where
1810    T: Clone + serde::Serialize + serde::de::DeserializeOwned + PartialEq
1811{
1812}