Skip to main content

hydro_lang/sim/
compiled.rs

1//! Interfaces for compiled Hydro simulators and concrete simulation instances.
2//!
3//! # Quiescence and observation soundness
4//!
5//! The scheduler distinguishes two kinds of simulation work:
6//! - **Deterministic work**: running the top-level async dataflows, which simply propagate
7//!   whatever data is already in flight. This makes no `nondet!` decisions, so running it can
8//!   never change which executions are explored.
9//! - **Nondeterministic work**: running ticks and observations, whose behavior depends on
10//!   decisions drawn from the bolero driver (batch boundaries, snapshot versions, message
11//!   orderings). Each decision forks the space of possible executions.
12//!
13//! The simulation is **quiescent** when neither kind of work can make progress without new
14//! external input. Test-side observations (the methods on [`SimReceiver`] /
15//! [`SimClusterReceiver`]) interact with the scheduler while waiting, and the key soundness
16//! question is: *when is it okay for an observation to let nondeterministic work run?*
17//!
18//! **Waiting for a message is always sound.** If the message eventually arrives, the work
19//! that ran was necessary to produce it (schedules that run *extra* work are also valid
20//! executions and are explored separately). If the simulation instead quiesces without
21//! producing the message, the assertion fails and the instance ends, so nothing can observe
22//! the overrun. This is why [`SimReceiver::next`], [`SimReceiver::collect_n`], and the
23//! `assert_yields*` prefix checks are safe to use in the middle of a test.
24//!
25//! **Observing the *absence* of a message is dangerous.** Proving that "no more messages can
26//! arrive" requires driving the simulation all the way to quiescence, running *all* pending
27//! nondeterministic work. A later assertion may have needed to observe a state where that
28//! work had not yet run — e.g., `assert_yields_only([1, 2])` followed by reading a counter
29//! must be able to see the counter *before* the ticks that count `1` and `2` have fired.
30//! Forcing quiescence at the first assertion would make some executions unobservable, and
31//! extra messages produced by the forced work could surface at a *later* assertion,
32//! misattributing the failure. Absence-observing APIs therefore proceed in phases:
33//!
34//! 1. **Settle** (see `SettlePauseGuard::poll_settle`): the scheduler runs only deterministic work, pausing
35//!    just before nondeterministic work. If the simulation reaches quiescence this way, the
36//!    end-of-stream check is *free* — no decision was forced, no execution was cut off — and
37//!    the test simply continues.
38//! 2. If nondeterministic work is pending, the check would overrun. What happens next depends
39//!    on the API and engine:
40//!    - The assertion APIs ([`SimReceiver::assert_no_more`], `assert_yields_only*`,
41//!      `collect_n_only`) under [`CompiledSim::exhaustive`] **fork** the search on a bolero
42//!      decision: one instance performs the check and then ends (via a discard panic, like
43//!      `sim::continue_if!`), while sibling instances skip the check entirely and continue. The
44//!      exhaustive driver enumerates the checking instance *first*, so a failing check is
45//!      found before any instance runs past it — with a decision trace that leads exactly to
46//!      the failing assertion. Since nothing after the check runs in the checking instance,
47//!      the overrun it performs is unobservable, and the continuing instances never quiesce,
48//!      so every downstream state remains reachable.
49//!    - Otherwise (fuzz / RNG / replay engines, or the drain-everything APIs
50//!      [`SimReceiver::try_next`], [`SimReceiver::collect`], and `collect_sorted` in every
51//!      mode), the pending work runs and the instance is **tainted**
52//!      (`QuiescenceState::tainted`). Reads of the now-quiescent state remain sound (they
53//!      observe a fully-drained simulation that can no longer advance), so tests may drain
54//!      multiple output ports at the end. But once new input is sent, the instance is
55//!      **poisoned** (`QuiescenceState::poisoned`): any further receive panics (see
56//!      `guard_not_poisoned`), because a failure observed after the forced overrun could
57//!      have been caused by it and attributed to the wrong assertion.
58//!
59//! NOTE: This module runs inside bolero's `catch_unwind` scope, which silently
60//! swallows panics. Internal invariant checks should use `abort_assert!`
61//! rather than `panic!`/`assert!`.
62//!
63//! TODO(mingwei): Panics inside the tick DFIR (generated code in the dylib) are
64//! also caught by bolero's `catch_unwind`. Consider a mechanism to detect and
65//! propagate those as well.
66
67/// Like `assert!`, but calls `std::process::abort()` instead of `panic!()`.
68/// Use for internal invariants that must not be silently caught by bolero.
69macro_rules! abort_assert {
70    ($cond:expr, $($arg:tt)*) => {
71        if !$cond {
72            eprintln!("Simulator internal error: {}", format!($($arg)*));
73            std::process::abort();
74        }
75    };
76}
77
78use core::{fmt, panic};
79use std::cell::{Cell, RefCell};
80use std::collections::{HashMap, VecDeque};
81use std::fmt::Debug;
82use std::panic::RefUnwindSafe;
83use std::path::Path;
84use std::pin::{Pin, pin};
85use std::rc::Rc;
86use std::task::{Poll, ready};
87
88use bytes::Bytes;
89use colored::Colorize;
90use dfir_rs::scheduled::context::DfirErased;
91use dfir_rs::util::unsync::mpsc::{Receiver as UnsyncReceiver, Sender as UnsyncSender};
92use futures::StreamExt;
93use libloading::Library;
94use serde::Serialize;
95use serde::de::DeserializeOwned;
96use tempfile::TempPath;
97use tokio::sync::{Mutex, Notify};
98
99use super::runtime::{Hooks, InlineHooks};
100use super::{SimClusterReceiver, SimClusterSender, SimReceiver, SimSender};
101use crate::compile::builder::ExternalPortId;
102use crate::live_collections::stream::{ExactlyOnce, NoOrder, Ordering, Retries, TotalOrder};
103use crate::location::dynamic::LocationId;
104use crate::sim::graph::{SimExternalPort, SimExternalPortRegistry};
105use crate::sim::runtime::SimHook;
106
107struct QuiescenceState {
108    /// Set to true when the scheduler reaches quiescence; reset to false when new input is sent.
109    quiescent: Cell<bool>,
110    /// Notified when the scheduler reaches quiescence (wakes receivers waiting for data).
111    quiescence_notify: Notify,
112    /// Notified when new input is sent, signaling the scheduler to resume.
113    resume_notify: Notify,
114    /// When nonzero, the scheduler must not start nondeterministic work (ticks /
115    /// observations): once only such work remains, it sets `nondet_pending` and pauses until
116    /// resumed. Used by receivers to query whether the simulation can quiesce
117    /// deterministically. This is a count (not a bool) because multiple settling futures can
118    /// be in flight at once (e.g. `select!`/`join!` between two receiver awaits): the
119    /// scheduler must stay paused until *every* one of them has finished settling.
120    pause_nondet: Cell<usize>,
121    /// Set while the scheduler is paused because nondeterministic work is ready to run but
122    /// `pause_nondet` is set.
123    nondet_pending: Cell<bool>,
124    /// Wakers for test-side tasks waiting for the scheduler to settle (either quiesce or set
125    /// `nondet_pending`) while `pause_nondet` is set.
126    settle_wakers: RefCell<Vec<std::task::Waker>>,
127    /// Set when an observation *forced* the simulation to quiesce (running pending
128    /// nondeterministic work) outside of exhaustive mode's forking. Further observations of
129    /// the quiescent state remain sound, but once new input is sent (see `poisoned`), later
130    /// observations could misattribute failures caused by the forced overrun.
131    tainted: Cell<bool>,
132    /// Set when new input is sent after `tainted`; all further receives panic.
133    poisoned: Cell<bool>,
134}
135
136impl QuiescenceState {
137    /// Signal that new input has been sent, waking the scheduler if it was quiescent.
138    fn resume(&self) {
139        if self.tainted.get() {
140            self.poisoned.set(true);
141        }
142        self.quiescent.set(false);
143        self.resume_notify.notify_waiters();
144    }
145
146    /// Whether the scheduler is currently quiescent (no more progress possible without input).
147    fn is_quiescent(&self) -> bool {
148        self.quiescent.get()
149    }
150
151    /// Returns a future that completes when the scheduler next reaches quiescence.
152    fn notified(&self) -> tokio::sync::futures::Notified<'_> {
153        self.quiescence_notify.notified()
154    }
155
156    /// Wakes test-side tasks waiting for the scheduler to settle.
157    fn wake_settled(&self) {
158        for waker in self.settle_wakers.borrow_mut().drain(..) {
159            waker.wake();
160        }
161    }
162
163    /// Enter quiescence and wait for new input before continuing.
164    async fn wait_for_resume(&self) {
165        self.quiescent.set(true);
166        self.quiescence_notify.notify_waiters();
167        self.wake_settled();
168        self.resume_notify.notified().await;
169        self.quiescent.set(false);
170    }
171}
172
173/// Tracks a pending "settle" pause request to the scheduler (see
174/// [`QuiescenceState::pause_nondet`]), releasing it if the requesting future is dropped
175/// mid-settle (e.g. by `select!`) so the scheduler is not left paused forever. Pause
176/// requests are counted, so concurrent settling futures each hold their own request.
177struct SettlePauseGuard {
178    quiescence: Rc<QuiescenceState>,
179    active: bool,
180}
181
182impl SettlePauseGuard {
183    fn new(quiescence: Rc<QuiescenceState>) -> Self {
184        SettlePauseGuard {
185            quiescence,
186            active: false,
187        }
188    }
189
190    fn acquire(&mut self) {
191        abort_assert!(!self.active, "settle pause acquired twice");
192        self.quiescence
193            .pause_nondet
194            .set(self.quiescence.pause_nondet.get() + 1);
195        self.active = true;
196    }
197
198    fn release(&mut self) {
199        abort_assert!(self.active, "settle pause released without being acquired");
200        self.active = false;
201        self.quiescence
202            .pause_nondet
203            .set(self.quiescence.pause_nondet.get() - 1);
204    }
205
206    /// Polls the "settle" handshake with the scheduler: deterministic (non-tick) work is
207    /// allowed to run, but the scheduler pauses instead of starting nondeterministic work
208    /// (ticks / observations). Resolves to `true` if the simulation reached quiescence
209    /// deterministically, or `false` if nondeterministic work is pending (in which case the
210    /// scheduler is resumed).
211    fn poll_settle(&mut self, cx: &mut std::task::Context<'_>) -> Poll<bool> {
212        let quiescence = self.quiescence.clone();
213        if !self.active {
214            if quiescence.is_quiescent() {
215                return Poll::Ready(true);
216            }
217            self.acquire();
218        }
219
220        if quiescence.is_quiescent() {
221            self.release();
222            Poll::Ready(true)
223        } else if quiescence.nondet_pending.get() {
224            self.release();
225            quiescence.resume_notify.notify_waiters();
226            Poll::Ready(false)
227        } else {
228            // This may push a duplicate waker if we are re-polled without an intervening
229            // `wake_settled` (e.g. a `join!` sibling waking the shared task), but duplicates
230            // are harmless (waking is idempotent) and are cleared at the next `wake_settled`,
231            // so deduplicating here isn't worth the scan on every poll.
232            quiescence
233                .settle_wakers
234                .borrow_mut()
235                .push(cx.waker().clone());
236            Poll::Pending
237        }
238    }
239}
240
241impl Drop for SettlePauseGuard {
242    fn drop(&mut self) {
243        if self.active {
244            self.release();
245            // Resume the scheduler in case this was the last pause request (otherwise it
246            // would stay parked forever with nobody left to resume it). If other settlers
247            // still hold requests, this wakeup is spurious but harmless: the scheduler
248            // re-checks `pause_nondet > 0` before starting any nondeterministic work, so it
249            // immediately re-parks without running anything.
250            self.quiescence.resume_notify.notify_waiters();
251        }
252    }
253}
254
255/// Panics if the simulation has been poisoned: an earlier observation forced the simulation
256/// to quiesce (running pending nondeterministic work), and new input has been sent since, so
257/// further observations could misattribute failures caused by the forced overrun.
258fn guard_not_poisoned(quiescence: &QuiescenceState) {
259    if quiescence.poisoned.get() {
260        panic!(
261            "cannot receive more simulator output: an earlier observation (such as `try_next`, `collect`, or a quiescence assertion outside exhaustive mode) forced the simulation to quiesce by running pending nondeterministic work, and new input has been sent since. Failures observed now could be misattributed, so either restructure the test to make quiescence-forcing observations its last step, or insert an explicit `sim::quiesce().await` phase barrier before sending more input."
262        );
263    }
264}
265
266/// Runs the simulation to quiescence, as an explicit *phase barrier* between rounds of a
267/// multi-phase test.
268///
269/// All pending nondeterministic work (ticks / observations) is forced to run until no more
270/// progress is possible without new input. This deliberately narrows the explored executions:
271/// inputs sent after the barrier will never interleave with work from before it, modeling
272/// scenarios where new stimuli (such as timer ticks) arrive long after the system settles.
273/// Pair such tests with a separate barrier-free test if interleaved executions should also be
274/// explored.
275///
276/// Because the barrier is explicit, observations after it are *intended* to see the fully
277/// settled state, so — unlike [`SimReceiver::try_next`] / [`SimReceiver::collect`] forcing
278/// quiescence implicitly — it does not restrict what the test may do afterwards: receives
279/// after the barrier observe only buffered output (plus whatever later input produces), and
280/// failures cannot be misattributed across it.
281pub async fn quiesce() {
282    let quiescence =
283        CURRENT_SIM_CONNECTIONS.with(|connections| connections.borrow().quiescence.clone());
284    guard_not_poisoned(&quiescence);
285
286    let mut notified_fut = pin!(None);
287    std::future::poll_fn(|cx| {
288        if quiescence.is_quiescent() {
289            return Poll::Ready(());
290        }
291        // Registered before the scheduler can run (single-threaded), so the quiescence
292        // notification cannot be missed.
293        if notified_fut.is_none() {
294            notified_fut.set(Some(quiescence.notified()));
295        }
296        let () = ready!(notified_fut.as_mut().as_pin_mut().unwrap().poll(cx));
297        Poll::Ready(())
298    })
299    .await;
300
301    // The barrier subsumes any quiescence forced by earlier observations in this phase:
302    // everything before it has fully settled, and the test has explicitly opted into
303    // observing only post-quiescence states from here on.
304    quiescence.tainted.set(false);
305}
306
307/// Receives the next message from `receiver` while trying not to overrun the simulation:
308/// first the simulation *settles* (deterministic work runs, but the scheduler pauses before
309/// nondeterministic work). If a message arrives, it is returned; if the simulation settles to
310/// quiescence, returns `None` without having run any nondeterministic work. Otherwise the
311/// scheduler is resumed and pending nondeterministic work runs until a message arrives or the
312/// simulation quiesces; quiescing this way *taints* the simulation (see
313/// [`QuiescenceState::tainted`]).
314async fn try_next_bytes(
315    receiver: &Mutex<UnsyncReceiver<Bytes>>,
316    quiescence: &Rc<QuiescenceState>,
317) -> Option<Bytes> {
318    guard_not_poisoned(quiescence);
319
320    let mut receiver_stream = receiver.lock().await;
321    let mut settle_guard = SettlePauseGuard::new(quiescence.clone());
322    // `Some` once the settle phase has concluded that nondeterministic work is pending and
323    // we have started forcing it to run.
324    let mut notified_fut = pin!(None);
325
326    std::future::poll_fn(|cx| {
327        // A message may become available at any point (including from deterministic work
328        // while settling), so always check the stream first.
329        match receiver_stream.poll_next_unpin(cx) {
330            Poll::Ready(Some(bytes)) => return Poll::Ready(Some(bytes)),
331            Poll::Ready(None) => return Poll::Ready(None),
332            Poll::Pending => {}
333        }
334
335        if notified_fut.is_none() {
336            match settle_guard.poll_settle(cx) {
337                // Deterministically quiescent: no more messages, and nothing was overrun.
338                Poll::Ready(true) => return Poll::Ready(None),
339                // Nondeterministic work is pending; start forcing it to run. The `Notified`
340                // is created here and polled (registered) below in this same synchronous
341                // poll — before the scheduler can run — and the simulation is not currently
342                // quiescent, so the quiescence notification cannot be missed.
343                Poll::Ready(false) => notified_fut.set(Some(quiescence.notified())),
344                Poll::Pending => return Poll::Pending,
345            }
346        }
347
348        // Let the scheduler run nondeterministic work until a message arrives or the
349        // simulation quiesces. Note that merely entering this phase does not taint: if a
350        // message arrives (the `Some` exit at the top), waiting was sound for the same
351        // reason as `SimReceiver::next` — the work that ran was needed to produce it. Only
352        // *observing quiescence* after forcing the pending work taints, since that is the
353        // overrun a later observation could misattribute.
354        let () = ready!(notified_fut.as_mut().as_pin_mut().unwrap().poll(cx));
355        quiescence.tainted.set(true);
356        Poll::Ready(None)
357    })
358    .await
359}
360
361struct SimConnections {
362    input_senders: HashMap<SimExternalPort, UnsyncSender<Bytes>>,
363    output_receivers: HashMap<SimExternalPort, Rc<Mutex<UnsyncReceiver<Bytes>>>>,
364    cluster_input_senders: HashMap<SimExternalPort, HashMap<u32, UnsyncSender<Bytes>>>,
365    cluster_output_receivers:
366        HashMap<SimExternalPort, HashMap<u32, Rc<Mutex<UnsyncReceiver<Bytes>>>>>,
367    external_registered: HashMap<ExternalPortId, SimExternalPort>,
368    quiescence: Rc<QuiescenceState>,
369    log: bool,
370    /// Whether this instance is being executed by the exhaustive engine (see
371    /// [`CompiledSim::exhaustive`]), which affects how `assert_yields_only` explores
372    /// quiescence checks.
373    exhaustive: bool,
374}
375
376/// Implementation detail of [`crate::sim::continue_if!`](crate::continue_if); do not call directly.
377///
378/// If `condition` is false, aborts the current simulation instance by panicking with a special
379/// payload ([`bolero::generator::bolero_generator::any::Error`]) that bolero recognizes as an
380/// "invalid input" marker: the instance is discarded (not treated as a test failure, and never
381/// recorded as a reproducer) and exploration moves on to the next instance. If logging is
382/// enabled for the current instance, the failed assumption is logged first.
383#[doc(hidden)]
384#[track_caller]
385pub fn continue_if_impl(condition: bool, message: fmt::Arguments<'_>) {
386    if condition {
387        return;
388    }
389
390    let log = CURRENT_SIM_CONNECTIONS
391        .try_with(|connections| connections.borrow().log)
392        .unwrap_or(true);
393    if log {
394        eprintln!(
395            "{}",
396            render_continue_if_failure(std::panic::Location::caller(), message)
397        );
398    }
399
400    // Panics with `bolero_generator::any::Error`, which bolero's engines treat as an invalid
401    // input rather than a test failure. Both this function and bolero's `assume` are
402    // `#[track_caller]`, so the recorded location is the user's `continue_if!` call site.
403    bolero::generator::bolero_generator::any::assume(false, "simulation assumption failed");
404}
405
406/// Renders the log message for a failed assumption, echoing the source line with a caret
407/// pointing at the `continue_if!` call site, in the same style as the other simulator logs.
408fn render_continue_if_failure(
409    location: &std::panic::Location<'_>,
410    message: fmt::Arguments<'_>,
411) -> String {
412    use std::fmt::Write;
413
414    // `Location::file()` is relative to the directory the crate was compiled from (e.g. the
415    // workspace root), which may not match the current working directory (e.g. the crate
416    // root when running `cargo test`), so walk up from the current directory to find it.
417    let source_line = std::env::current_dir()
418        .ok()
419        .and_then(|cwd| {
420            cwd.ancestors()
421                .find_map(|base| std::fs::read_to_string(base.join(location.file())).ok())
422        })
423        .and_then(|content| {
424            content
425                .lines()
426                .nth((location.line() as usize).saturating_sub(1))
427                .map(|line| line.to_owned())
428        })
429        .unwrap_or_default();
430
431    let caret_indent = " ".repeat((location.column() as usize).saturating_sub(1));
432
433    let mut out = String::new();
434    let _ = writeln!(
435        out,
436        "\n{}",
437        "Condition failed (discarding simulation instance):"
438            .color(colored::Color::Yellow)
439            .bold()
440    );
441    let _ = writeln!(out, "{} {}", "-->".color(colored::Color::Blue), location);
442    let _ = writeln!(out, " {}{}", "|".color(colored::Color::Blue), source_line);
443    let _ = write!(
444        out,
445        " {}{}{}",
446        "|".color(colored::Color::Blue),
447        caret_indent,
448        format!("^ {}", message).color(colored::Color::Yellow)
449    );
450    out
451}
452
453tokio::task_local! {
454    static CURRENT_SIM_CONNECTIONS: RefCell<SimConnections>;
455}
456
457/// A handle to a compiled Hydro simulation, which can be instantiated and run.
458pub struct CompiledSim {
459    pub(super) _path: TempPath,
460    pub(super) lib: Library,
461    pub(super) externals_port_registry: SimExternalPortRegistry,
462    pub(super) unit_test_fuzz_iterations: usize,
463}
464
465#[sealed::sealed]
466/// A trait implemented by closures that can instantiate a compiled simulation.
467///
468/// This is needed to ensure [`RefUnwindSafe`] so instances can be created during fuzzing.
469pub trait Instantiator<'a>: RefUnwindSafe + Fn() -> CompiledSimInstance<'a> {}
470#[sealed::sealed]
471impl<'a, T: RefUnwindSafe + Fn() -> CompiledSimInstance<'a>> Instantiator<'a> for T {}
472
473fn null_handler(_args: fmt::Arguments) {}
474
475fn println_handler(args: fmt::Arguments) {
476    println!("{}", args);
477}
478
479fn eprintln_handler(args: fmt::Arguments) {
480    eprintln!("{}", args);
481}
482
483/// Creates a simulation instance, returning:
484/// - A list of async DFIRs to run (all process / cluster logic outside a tick)
485/// - A list of tick DFIRs to run (where the &'static str is for the tick location id)
486/// - A mapping of hooks for non-deterministic decisions at tick-input boundaries
487/// - A mapping of inline hooks for non-deterministic decisions inside ticks
488type SimLoaded<'a> = libloading::Symbol<
489    'a,
490    unsafe extern "Rust" fn(
491        should_color: bool,
492        external_out: &mut HashMap<usize, UnsyncReceiver<Bytes>>,
493        external_in: &mut HashMap<usize, UnsyncSender<Bytes>>,
494        cluster_external_out: &mut HashMap<usize, HashMap<u32, UnsyncReceiver<Bytes>>>,
495        cluster_external_in: &mut HashMap<usize, HashMap<u32, UnsyncSender<Bytes>>>,
496        println_handler: fn(fmt::Arguments<'_>),
497        eprintln_handler: fn(fmt::Arguments<'_>),
498    ) -> (
499        Vec<(&'static str, Option<u32>, DfirErased)>,
500        Vec<(&'static str, Option<u32>, DfirErased)>,
501        Hooks<&'static str>,
502        InlineHooks<&'static str>,
503    ),
504>;
505
506impl CompiledSim {
507    /// Executes the given closure with a single instance of the compiled simulation.
508    pub fn with_instance<T>(&self, thunk: impl FnOnce(CompiledSimInstance) -> T) -> T {
509        self.with_instantiator(|instantiator| thunk(instantiator()), true)
510    }
511
512    /// Executes the given closure with an [`Instantiator`], which can be called to create
513    /// independent instances of the simulation. This is useful for fuzzing, where we need to
514    /// re-execute the simulation several times with different decisions.
515    ///
516    /// The `always_log` parameter controls whether to log tick executions and stream releases. If
517    /// it is `true`, logging will always be enabled. If it is `false`, logging will only be
518    /// enabled if the `HYDRO_SIM_LOG` environment variable is set to `1`.
519    pub fn with_instantiator<T>(
520        &self,
521        thunk: impl FnOnce(&dyn Instantiator) -> T,
522        always_log: bool,
523    ) -> T {
524        let func: SimLoaded = unsafe { self.lib.get(b"__hydro_runtime").unwrap() };
525        let log = always_log || std::env::var("HYDRO_SIM_LOG").is_ok_and(|v| v == "1");
526        thunk(
527            &(|| CompiledSimInstance {
528                func: func.clone(),
529                externals_port_registry: self.externals_port_registry.clone(),
530                dylib_result: None,
531                log,
532                exhaustive: false,
533            }),
534        )
535    }
536
537    /// Uses a fuzzing strategy to explore possible executions of the simulation. The provided
538    /// closure will be repeatedly executed with instances of the Hydro program where the
539    /// batching boundaries, order of messages, and retries are varied.
540    ///
541    /// During development, you should run the test that invokes this function with the `cargo sim`
542    /// command, which will use `libfuzzer` to intelligently explore the execution space. If a
543    /// failure is found, a minimized test case will be produced in a `sim-failures` directory.
544    /// When running the test with `cargo test` (such as in CI), if a reproducer is found it will
545    /// be executed, and if no reproducer is found a small number of random executions will be
546    /// performed.
547    pub fn fuzz(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) {
548        let caller_fn = crate::compile::ir::backtrace::Backtrace::get_backtrace(0)
549            .elements()
550            .into_iter()
551            .find(|e| {
552                !e.fn_name.starts_with("hydro_lang::sim::compiled")
553                    && !e.fn_name.starts_with("hydro_lang::sim::flow")
554                    && !e.fn_name.starts_with("fuzz<")
555                    && !e.fn_name.starts_with("<hydro_lang::sim")
556            })
557            .unwrap();
558
559        let caller_path = Path::new(&caller_fn.filename.unwrap()).to_path_buf();
560        let repro_folder = caller_path.parent().unwrap().join("sim-failures");
561
562        let caller_fuzz_repro_path = repro_folder
563            .join(caller_fn.fn_name.replace("::", "__"))
564            .with_extension("bin");
565
566        if std::env::var("BOLERO_FUZZER").is_ok() {
567            let corpus_dir = std::env::current_dir().unwrap().join(".fuzz-corpus");
568            std::fs::create_dir_all(&corpus_dir).unwrap();
569            let libfuzzer_args = format!(
570                "{} {} -artifact_prefix={}/ -handle_abrt=0",
571                corpus_dir.to_str().unwrap(),
572                corpus_dir.to_str().unwrap(),
573                corpus_dir.to_str().unwrap(),
574            );
575
576            std::fs::create_dir_all(&repro_folder).unwrap();
577
578            if !std::env::var("HYDRO_NO_FAILURE_OUTPUT").is_ok_and(|v| v == "1") {
579                unsafe {
580                    std::env::set_var(
581                        "BOLERO_FAILURE_OUTPUT",
582                        caller_fuzz_repro_path.to_str().unwrap(),
583                    );
584                }
585            }
586
587            unsafe {
588                std::env::set_var("BOLERO_LIBFUZZER_ARGS", libfuzzer_args);
589            }
590
591            self.with_instantiator(
592                |instantiator| {
593                    bolero::test(bolero::TargetLocation {
594                        package_name: "",
595                        manifest_dir: "",
596                        module_path: "",
597                        file: "",
598                        line: 0,
599                        item_path: "<unknown>::__bolero_item_path__",
600                        test_name: None,
601                    })
602                    .run_with_replay(move |is_replay| {
603                        let mut instance = instantiator();
604
605                        if instance.log {
606                            eprintln!(
607                                "{}",
608                                "\n==== New Simulation Instance ===="
609                                    .color(colored::Color::Cyan)
610                                    .bold()
611                            );
612                        }
613
614                        if is_replay {
615                            instance.log = true;
616                        }
617
618                        tokio::runtime::Builder::new_current_thread()
619                            .build()
620                            .unwrap()
621                            .block_on(async { instance.run(&mut thunk).await })
622                    })
623                },
624                false,
625            );
626        } else if let Ok(existing_bytes) = std::fs::read(&caller_fuzz_repro_path) {
627            self.fuzz_repro(existing_bytes, async |compiled| {
628                compiled.launch();
629                thunk().await
630            });
631        } else {
632            eprintln!(
633                "Running a fuzz test without `cargo sim` and no reproducer found at {}, using {} iterations with random inputs.",
634                caller_fuzz_repro_path.display(),
635                self.unit_test_fuzz_iterations,
636            );
637            self.with_instantiator(
638                |instantiator| {
639                    bolero::test(bolero::TargetLocation {
640                        package_name: "",
641                        manifest_dir: "",
642                        module_path: "",
643                        file: ".",
644                        line: 0,
645                        item_path: "<unknown>::__bolero_item_path__",
646                        test_name: None,
647                    })
648                    .with_iterations(self.unit_test_fuzz_iterations)
649                    .run_with_replay(move |is_replay| {
650                        let mut instance = instantiator();
651
652                        if instance.log {
653                            eprintln!(
654                                "{}",
655                                "\n==== New Simulation Instance ===="
656                                    .color(colored::Color::Cyan)
657                                    .bold()
658                            );
659                        }
660
661                        if is_replay {
662                            instance.log = true;
663                        }
664
665                        tokio::runtime::Builder::new_current_thread()
666                            .build()
667                            .unwrap()
668                            .block_on(async { instance.run(&mut thunk).await })
669                    })
670                },
671                false,
672            );
673        }
674    }
675
676    /// Executes the given closure with a single instance of the compiled simulation, using the
677    /// provided bytes as the source of fuzzing decisions. This can be used to manually reproduce a
678    /// failure found during fuzzing.
679    pub fn fuzz_repro<'a>(
680        &'a self,
681        bytes: Vec<u8>,
682        thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
683    ) {
684        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
685            self.with_instance(|instance| {
686                bolero::bolero_engine::any::scope::with(
687                    Box::new(bolero::bolero_engine::driver::object::Object(
688                        bolero::bolero_engine::driver::bytes::Driver::new(
689                            bytes,
690                            &Default::default(),
691                        ),
692                    )),
693                    || {
694                        tokio::runtime::Builder::new_current_thread()
695                            .build()
696                            .unwrap()
697                            .block_on(async { instance.run_without_launching(thunk).await })
698                    },
699                )
700            })
701        }));
702
703        if let Err(payload) = result {
704            if payload
705                .downcast_ref::<bolero::generator::bolero_generator::any::Error>()
706                .is_some()
707            {
708                // A `continue_if!` failed (or the driver ran out of entropy) while replaying the
709                // recorded bytes. Instances that fail an assumption are never recorded as
710                // failures, so this means the reproducer is stale or does not correspond to
711                // this program.
712                panic!(
713                    "simulation assumption failed while replaying recorded fuzz decisions; the reproducer may be stale or may not correspond to this program"
714                );
715            }
716            std::panic::resume_unwind(payload);
717        }
718    }
719
720    /// Exhaustively searches all possible executions of the simulation. The provided
721    /// closure will be repeatedly executed with instances of the Hydro program where the
722    /// batching boundaries, order of messages, and retries are varied.
723    ///
724    /// Exhaustive searching is feasible when the inputs to the Hydro program are finite and there
725    /// are no dataflow loops that generate infinite messages. Exhaustive searching provides a
726    /// stronger guarantee of correctness than fuzzing, but may take a long time to complete.
727    /// Because no fuzzer is involved, you can run exhaustive tests with `cargo test`.
728    ///
729    /// Returns the number of distinct executions explored.
730    pub fn exhaustive(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) -> usize {
731        if std::env::var("BOLERO_FUZZER").is_ok() {
732            eprintln!(
733                "Cannot run exhaustive tests with a fuzzer. Please use `cargo test` instead of `cargo sim`."
734            );
735            std::process::abort();
736        }
737
738        let mut count = 0;
739        let count_mut = &mut count;
740
741        let _span = tracing::debug_span!(target: "hydro_build", "sim_exhaustive").entered();
742
743        self.with_instantiator(
744            |instantiator| {
745                bolero::test(bolero::TargetLocation {
746                    package_name: "",
747                    manifest_dir: "",
748                    module_path: "",
749                    file: "",
750                    line: 0,
751                    item_path: "<unknown>::__bolero_item_path__",
752                    test_name: None,
753                })
754                .exhaustive()
755                .run_with_replay(move |is_replay| {
756                    *count_mut += 1;
757
758                    let mut instance = instantiator();
759                    instance.exhaustive = true;
760                    if instance.log {
761                        eprintln!(
762                            "{}",
763                            "\n==== New Simulation Instance ===="
764                                .color(colored::Color::Cyan)
765                                .bold()
766                        );
767                    }
768
769                    if is_replay {
770                        instance.log = true;
771                    }
772
773                    tokio::runtime::Builder::new_current_thread()
774                        .build()
775                        .unwrap()
776                        .block_on(async { instance.run(&mut thunk).await })
777                })
778            },
779            false,
780        );
781
782        count
783    }
784}
785
786// This must be a tuple because it is referenced from generated code in `graph.rs`.
787type DylibResult = (
788    Vec<(&'static str, Option<u32>, DfirErased)>,
789    Vec<(&'static str, Option<u32>, DfirErased)>,
790    Hooks<&'static str>,
791    InlineHooks<&'static str>,
792);
793
794/// A single instance of a compiled Hydro simulation, which provides methods to interactively
795/// execute the simulation, feed inputs, and receive outputs.
796pub struct CompiledSimInstance<'a> {
797    func: SimLoaded<'a>,
798    externals_port_registry: SimExternalPortRegistry,
799    dylib_result: Option<DylibResult>,
800    log: bool,
801    exhaustive: bool,
802}
803
804impl<'a> CompiledSimInstance<'a> {
805    async fn run(self, thunk: impl AsyncFnOnce() + RefUnwindSafe) {
806        self.run_without_launching(async |instance| {
807            instance.launch();
808            thunk().await;
809        })
810        .await;
811    }
812
813    async fn run_without_launching(
814        mut self,
815        thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
816    ) {
817        let mut external_out: HashMap<usize, UnsyncReceiver<Bytes>> = HashMap::new();
818        let mut external_in: HashMap<usize, UnsyncSender<Bytes>> = HashMap::new();
819        let mut cluster_external_out: HashMap<usize, HashMap<u32, UnsyncReceiver<Bytes>>> =
820            HashMap::new();
821        let mut cluster_external_in: HashMap<usize, HashMap<u32, UnsyncSender<Bytes>>> =
822            HashMap::new();
823
824        let dylib_result = unsafe {
825            (self.func)(
826                colored::control::SHOULD_COLORIZE.should_colorize(),
827                &mut external_out,
828                &mut external_in,
829                &mut cluster_external_out,
830                &mut cluster_external_in,
831                if self.log {
832                    println_handler
833                } else {
834                    null_handler
835                },
836                if self.log {
837                    eprintln_handler
838                } else {
839                    null_handler
840                },
841            )
842        };
843
844        let registered = &self.externals_port_registry.registered;
845
846        let quiescence = Rc::new(QuiescenceState {
847            quiescent: Cell::new(false),
848            quiescence_notify: Notify::new(),
849            resume_notify: Notify::new(),
850            pause_nondet: Cell::new(0),
851            nondet_pending: Cell::new(false),
852            settle_wakers: RefCell::new(vec![]),
853            tainted: Cell::new(false),
854            poisoned: Cell::new(false),
855        });
856
857        let mut input_senders = HashMap::new();
858        let mut output_receivers = HashMap::new();
859        let mut cluster_input_senders = HashMap::new();
860        let mut cluster_output_receivers = HashMap::new();
861
862        #[expect(
863            clippy::disallowed_methods,
864            reason = "inserts into maps also unordered"
865        )]
866        for sim_port in registered.values() {
867            let usize_key = sim_port.into_inner();
868            if let Some(sender) = external_in.remove(&usize_key) {
869                input_senders.insert(*sim_port, sender);
870            }
871            if let Some(receiver) = external_out.remove(&usize_key) {
872                output_receivers.insert(*sim_port, Rc::new(Mutex::new(receiver)));
873            }
874            if let Some(senders) = cluster_external_in.remove(&usize_key) {
875                cluster_input_senders.insert(*sim_port, senders);
876            }
877            if let Some(receivers) = cluster_external_out.remove(&usize_key) {
878                cluster_output_receivers.insert(
879                    *sim_port,
880                    receivers
881                        .into_iter()
882                        .map(|(member, r)| (member, Rc::new(Mutex::new(r))))
883                        .collect(),
884                );
885            }
886        }
887
888        self.dylib_result = Some(dylib_result);
889
890        let local_set = tokio::task::LocalSet::new();
891        local_set
892            .run_until(CURRENT_SIM_CONNECTIONS.scope(
893                RefCell::new(SimConnections {
894                    input_senders,
895                    output_receivers,
896                    cluster_input_senders,
897                    cluster_output_receivers,
898                    external_registered: self.externals_port_registry.registered.clone(),
899                    quiescence: quiescence.clone(),
900                    log: self.log,
901                    exhaustive: self.exhaustive,
902                }),
903                async move {
904                    thunk(self).await;
905                },
906            ))
907            .await;
908    }
909
910    /// Launches the simulation, which will asynchronously simulate the Hydro program. This should
911    /// be invoked but before receiving any messages.
912    fn launch(self) {
913        tokio::task::spawn_local(self.schedule_with_maybe_logger::<std::io::Empty>(None));
914    }
915
916    /// Returns a future that schedules simulation with the given logger for reporting the
917    /// simulation trace.
918    pub fn schedule_with_logger<W: std::io::Write>(
919        self,
920        log_writer: W,
921    ) -> impl use<W> + Future<Output = ()> {
922        self.schedule_with_maybe_logger(Some(log_writer))
923    }
924
925    fn schedule_with_maybe_logger<W: std::io::Write>(
926        mut self,
927        log_override: Option<W>,
928    ) -> impl use<W> + Future<Output = ()> {
929        let (async_dfirs, tick_dfirs, hooks, inline_hooks) = self.dylib_result.take().unwrap();
930
931        let not_ready_observation = async_dfirs
932            .iter()
933            .map(|(lid, c_id, _)| (serde_json::from_str(lid).unwrap(), *c_id))
934            .collect();
935
936        let quiescence = CURRENT_SIM_CONNECTIONS.with(|connections| {
937            let connections = connections.borrow();
938            connections.quiescence.clone()
939        });
940
941        let mut launched = LaunchedSim {
942            async_dfirs: async_dfirs
943                .into_iter()
944                .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
945                .collect(),
946            possibly_ready_ticks: vec![],
947            not_ready_ticks: tick_dfirs
948                .into_iter()
949                .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
950                .collect(),
951            possibly_ready_observation: vec![],
952            not_ready_observation,
953            hooks: hooks
954                .into_iter()
955                .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
956                .collect(),
957            inline_hooks: inline_hooks
958                .into_iter()
959                .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
960                .collect(),
961            log: if self.log {
962                if let Some(w) = log_override {
963                    LogKind::Custom(w)
964                } else {
965                    LogKind::Stderr
966                }
967            } else {
968                LogKind::Null
969            },
970            quiescence,
971        };
972
973        async move { launched.scheduler().await }
974    }
975}
976
977impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone for SimReceiver<T, O, R> {
978    fn clone(&self) -> Self {
979        *self
980    }
981}
982
983impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy for SimReceiver<T, O, R> {}
984
985/// How a [`QuiescenceCheckFuture`] resolves the "did the stream end?" check of
986/// `assert_no_more`. Decided once the simulation has settled (run out of deterministic
987/// work).
988#[derive(Clone, Copy)]
989enum QuiescenceBranch {
990    /// Skip the check and continue the test. Only taken in exhaustive mode, where a
991    /// sibling instance performs the check instead.
992    Continue,
993    /// Perform the check, then end this simulation instance (exhaustive mode), letting
994    /// sibling instances continue past this point without forcing quiescence.
995    CheckThenEnd,
996    /// Perform the check and keep running. Taken when the simulation is already quiescent
997    /// (the check is free) and in non-exhaustive modes.
998    CheckAndKeepRunning,
999}
1000
1001/// Decides how to run the quiescence check when the simulation has pending nondeterministic
1002/// work (ticks / observations) that the check would force to run.
1003fn decide_quiescence_branch() -> QuiescenceBranch {
1004    let (exhaustive, log) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1005        let connections = connections.borrow();
1006        (connections.exhaustive, connections.log)
1007    });
1008
1009    if !exhaustive {
1010        return QuiescenceBranch::CheckAndKeepRunning;
1011    }
1012
1013    // In exhaustive mode, fork the search on a bolero decision. The exhaustive driver
1014    // enumerates `false` first, so the instance that performs the quiescence check is
1015    // explored *before* any instance that continues past this assertion. This ensures that
1016    // if the stream has extra output, the failure is attributed to this assertion (with a
1017    // decision trace leading exactly to the check) rather than leaking the extra messages
1018    // into a later assertion.
1019    let continue_without_check: bool = bolero::any();
1020    if continue_without_check {
1021        if log {
1022            eprintln!(
1023                "\n{}",
1024                "Continuing past quiescence assertion without checking (checked by an earlier instance)"
1025                    .color(colored::Color::Cyan)
1026                    .bold()
1027            );
1028        }
1029        QuiescenceBranch::Continue
1030    } else {
1031        if log {
1032            eprintln!(
1033                "\n{}",
1034                "Checking that no more messages arrive (this instance will end after the check)"
1035                    .color(colored::Color::Cyan)
1036                    .bold()
1037            );
1038        }
1039        QuiescenceBranch::CheckThenEnd
1040    }
1041}
1042
1043/// Ends the current simulation instance after a passing quiescence check, by panicking with
1044/// [`bolero::generator::bolero_generator::any::Error`], which bolero's engines treat as an
1045/// invalid input rather than a test failure. The instance has verified everything up to and
1046/// including the quiescence check; sibling instances continue past the check instead.
1047fn end_instance_after_quiescence_check() -> ! {
1048    bolero::generator::bolero_generator::any::assume(
1049        false,
1050        "simulation instance ended after quiescence check",
1051    );
1052    unreachable!()
1053}
1054
1055pin_project_lite::pin_project! {
1056    // The "and then the stream ends" half of `assert_no_more` (and thus of
1057    // `assert_yields_only*` / `collect_n_only`). First lets the simulation *settle* (see
1058    // `poll_settle`): if it settles to quiescence, the check is free and the test simply
1059    // continues. Otherwise, in exhaustive mode the search forks into a checking instance and
1060    // continuing instances (see `SimReceiver::assert_no_more` and
1061    // `decide_quiescence_branch`); in non-exhaustive modes the check runs, forcing the
1062    // pending work (which taints the simulation, via `try_next_bytes`).
1063    //
1064    // See [`FutureTrackingCaller`] for why `poll` is `#[track_caller]`.
1065    struct QuiescenceCheckFuture<F: Future<Output = ()>> {
1066        #[pin]
1067        check: F,
1068        settle: SettlePauseGuard,
1069        branch: Option<QuiescenceBranch>,
1070    }
1071}
1072
1073impl<F: Future<Output = ()>> QuiescenceCheckFuture<F> {
1074    fn new(check: F) -> Self {
1075        QuiescenceCheckFuture {
1076            check,
1077            settle: SettlePauseGuard::new(
1078                CURRENT_SIM_CONNECTIONS.with(|connections| connections.borrow().quiescence.clone()),
1079            ),
1080            branch: None,
1081        }
1082    }
1083}
1084
1085impl<F: Future<Output = ()>> Future for QuiescenceCheckFuture<F> {
1086    type Output = ();
1087
1088    #[track_caller]
1089    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1090        let this = self.as_mut().project();
1091
1092        if this.branch.is_none() {
1093            *this.branch = Some(if ready!(this.settle.poll_settle(cx)) {
1094                // Settled to quiescence deterministically, so the check is free.
1095                QuiescenceBranch::CheckAndKeepRunning
1096            } else {
1097                // The check would force nondeterministic work to run.
1098                decide_quiescence_branch()
1099            });
1100        }
1101
1102        match this.branch.unwrap() {
1103            QuiescenceBranch::Continue => Poll::Ready(()),
1104            QuiescenceBranch::CheckAndKeepRunning => this.check.poll(cx),
1105            QuiescenceBranch::CheckThenEnd => {
1106                ready!(this.check.poll(cx));
1107                end_instance_after_quiescence_check()
1108            }
1109        }
1110    }
1111}
1112
1113impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimReceiver<T, O, R> {
1114    fn connections(&self) -> (Rc<Mutex<UnsyncReceiver<Bytes>>>, Rc<QuiescenceState>) {
1115        CURRENT_SIM_CONNECTIONS.with(|connections| {
1116            let connections = connections.borrow();
1117            let port = connections.external_registered.get(&self.0).unwrap();
1118            (
1119                connections.output_receivers.get(port).unwrap().clone(),
1120                connections.quiescence.clone(),
1121            )
1122        })
1123    }
1124
1125    /// See [`try_next_bytes`].
1126    async fn try_next_impl(&self) -> Option<T> {
1127        let (receiver, quiescence) = self.connections();
1128        try_next_bytes(&receiver, &quiescence)
1129            .await
1130            .map(|bytes| bincode::deserialize(&bytes).unwrap())
1131    }
1132
1133    /// Asserts that the stream has ended and no more messages can possibly arrive.
1134    ///
1135    /// If the check cannot be answered without running pending nondeterministic work (such
1136    /// as ticks with buffered inputs):
1137    /// - Under [`CompiledSim::exhaustive`], the search forks: one instance performs the
1138    ///   check and ends there, while sibling instances skip the check and continue.
1139    /// - In other modes, the pending work runs; afterwards, sending more input and then
1140    ///   attempting to receive output will panic.
1141    pub fn assert_no_more(self) -> impl Future<Output = ()>
1142    where
1143        T: Debug,
1144    {
1145        QuiescenceCheckFuture::new(FutureTrackingCaller {
1146            future: async move {
1147                if let Some(next) = self.try_next_impl().await {
1148                    return Err(format!(
1149                        "Stream yielded unexpected message: {:?}, expected termination",
1150                        next
1151                    ));
1152                }
1153                Ok(())
1154            },
1155        })
1156    }
1157}
1158
1159impl<T: Serialize + DeserializeOwned> SimReceiver<T, TotalOrder, ExactlyOnce> {
1160    /// Receives the next message from the external bincode stream, waiting (and letting the
1161    /// scheduler run any pending simulation work) until one is available. If the simulation
1162    /// becomes quiescent without producing a message, the test fails.
1163    ///
1164    /// This is safe to use in the middle of a test; to observe the *absence* of a message,
1165    /// use [`Self::try_next`] or [`Self::assert_no_more`].
1166    pub fn next(&self) -> impl use<'_, T> + Future<Output = T> {
1167        // Waiting for a message never "overruns" the simulation, even though the scheduler
1168        // may run nondeterministic ticks while we wait: if a message arrives, some pending
1169        // work was necessary to produce it (schedules that run *extra* work are also valid
1170        // executions, explored separately), and if the simulation quiesces instead, the test
1171        // fails right here — so no later observation can be affected by the overrun (the
1172        // taint set by `try_next_impl` is unobservable). See the module docs for the full
1173        // soundness reasoning.
1174        FutureTrackingCaller {
1175            future: async move {
1176                self.try_next_impl().await.ok_or_else(|| {
1177                    "Stream ended (simulation quiescent), but another message was expected"
1178                        .to_owned()
1179                })
1180            },
1181        }
1182    }
1183
1184    /// Receives the next message from the external bincode stream, or returns `None` if no
1185    /// more messages can possibly arrive.
1186    ///
1187    /// If answering requires forcing pending nondeterministic work to run, then afterwards,
1188    /// sending more input and then attempting to receive output will panic. Prefer
1189    /// [`Self::next`] (or [`Self::assert_no_more`]) when possible.
1190    pub async fn try_next(&self) -> Option<T> {
1191        self.try_next_impl().await
1192    }
1193
1194    /// Receives the next `n` messages from the external bincode stream, waiting (and letting
1195    /// the scheduler run any pending simulation work) until they are available. If the
1196    /// simulation becomes quiescent before `n` messages arrive, the test fails.
1197    ///
1198    /// Like [`Self::next`], this is safe to use in the middle of a test. It does not check
1199    /// that the stream ends afterwards; use [`Self::collect_n_only`] for that.
1200    pub fn collect_n<C: Default + Extend<T>>(
1201        &self,
1202        n: usize,
1203    ) -> impl use<'_, T, C> + Future<Output = C> {
1204        FutureTrackingCaller {
1205            future: async move {
1206                let mut out = C::default();
1207                for i in 0..n {
1208                    // Like `next`, waiting for each message is safe mid-test; the taint on a
1209                    // forced `None` is unobservable because the test fails below.
1210                    if let Some(v) = self.try_next_impl().await {
1211                        out.extend([v]);
1212                    } else {
1213                        return Err(format!(
1214                            "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1215                            i, n
1216                        ));
1217                    }
1218                }
1219                Ok(out)
1220            },
1221        }
1222    }
1223
1224    /// Receives the next `n` messages (like [`Self::collect_n`]) and then asserts that the
1225    /// stream ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1226    pub async fn collect_n_only<C: Default + Extend<T>>(self, n: usize) -> C
1227    where
1228        T: Debug,
1229    {
1230        let out = self.collect_n(n).await;
1231        self.assert_no_more().await;
1232        out
1233    }
1234
1235    /// Collects all remaining messages from the external bincode stream into a collection,
1236    /// waiting until no more messages can possibly arrive.
1237    ///
1238    /// If this has to force pending nondeterministic work to run, it should be the last
1239    /// observation of the test: afterwards, sending more input and then attempting to
1240    /// receive output will panic. When the number of expected messages is known, prefer
1241    /// [`Self::collect_n`] / [`Self::collect_n_only`].
1242    pub async fn collect<C: Default + Extend<T>>(self) -> C {
1243        let mut out = C::default();
1244        while let Some(v) = self.try_next_impl().await {
1245            out.extend([v]);
1246        }
1247        out
1248    }
1249
1250    /// Asserts that the stream yields exactly the expected sequence of messages, in order.
1251    /// This does not check that the stream ends, use [`Self::assert_yields_only`] for that.
1252    ///
1253    /// Like [`Self::next`], this is safe to use in the middle of a test.
1254    pub fn assert_yields<T2: Debug, I: IntoIterator<Item = T2>>(
1255        &self,
1256        expected: I,
1257    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1258    where
1259        T: Debug + PartialEq<T2>,
1260    {
1261        FutureTrackingCaller {
1262            future: async {
1263                let mut expected: VecDeque<T2> = expected.into_iter().collect();
1264
1265                while !expected.is_empty() {
1266                    // Like `next`, waiting for each expected message is safe mid-test; the
1267                    // taint on a forced `None` is unobservable because the test fails below.
1268                    if let Some(next) = self.try_next_impl().await {
1269                        let next_expected = expected.pop_front().unwrap();
1270                        if next != next_expected {
1271                            return Err(format!(
1272                                "Stream yielded unexpected message: {:?}, expected: {:?}",
1273                                next, next_expected
1274                            ));
1275                        }
1276                    } else {
1277                        return Err(format!(
1278                            "Stream ended early, still expected: {:?}",
1279                            expected
1280                        ));
1281                    }
1282                }
1283
1284                Ok(())
1285            },
1286        }
1287    }
1288
1289    /// Asserts that the stream yields only the expected sequence of messages, in order,
1290    /// and then ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1291    pub fn assert_yields_only<T2: Debug, I: IntoIterator<Item = T2>>(
1292        &self,
1293        expected: I,
1294    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1295    where
1296        T: Debug + PartialEq<T2>,
1297    {
1298        ChainedFuture {
1299            first: self.assert_yields(expected),
1300            second: self.assert_no_more(),
1301            first_done: false,
1302        }
1303    }
1304}
1305
1306pin_project_lite::pin_project! {
1307    // A future that tracks the location of the `.await` call for better panic messages.
1308    //
1309    // `#[track_caller]` is important for us to create assertion methods because it makes
1310    // the panic backtrace show up at that method (instead of inside the call tree within
1311    // that method). This is e.g. what `Option::unwrap` uses. Unfortunately, `#[track_caller]`
1312    // does not work correctly for async methods (or `dyn Future` either), so we have to
1313    // create these concrete future types that (1) have `#[track_caller]` on their `poll()`
1314    // method and (2) have the `panic!` triggered in their `poll()` method (or in a directly
1315    // nested concrete future).
1316    struct FutureTrackingCaller<F> {
1317        #[pin]
1318        future: F,
1319    }
1320}
1321
1322impl<T, F: Future<Output = Result<T, String>>> Future for FutureTrackingCaller<F> {
1323    type Output = T;
1324
1325    #[track_caller]
1326    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1327        match ready!(self.as_mut().project().future.poll(cx)) {
1328            Ok(v) => Poll::Ready(v),
1329            Err(e) => panic!("{}", e),
1330        }
1331    }
1332}
1333
1334pin_project_lite::pin_project! {
1335    // A future that first awaits the first future, then the second, propagating caller info.
1336    //
1337    // See [`FutureTrackingCaller`] for context.
1338    struct ChainedFuture<F1: Future<Output = ()>, F2: Future<Output = ()>> {
1339        #[pin]
1340        first: F1,
1341        #[pin]
1342        second: F2,
1343        first_done: bool,
1344    }
1345}
1346
1347impl<F1: Future<Output = ()>, F2: Future<Output = ()>> Future for ChainedFuture<F1, F2> {
1348    type Output = ();
1349
1350    #[track_caller]
1351    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1352        if !self.first_done {
1353            ready!(self.as_mut().project().first.poll(cx));
1354            *self.as_mut().project().first_done = true;
1355        }
1356
1357        self.as_mut().project().second.poll(cx)
1358    }
1359}
1360
1361impl<T: Serialize + DeserializeOwned> SimReceiver<T, NoOrder, ExactlyOnce> {
1362    /// Receives the next `n` messages, sorted, waiting (and letting the scheduler run any
1363    /// pending simulation work) until they are available. If the simulation becomes quiescent
1364    /// before `n` messages arrive, the test fails.
1365    ///
1366    /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1367    pub fn collect_n_sorted<C: Default + Extend<T> + AsMut<[T]>>(
1368        &self,
1369        n: usize,
1370    ) -> impl use<'_, T, C> + Future<Output = C>
1371    where
1372        T: Ord,
1373    {
1374        FutureTrackingCaller {
1375            future: async move {
1376                let mut out = C::default();
1377                for i in 0..n {
1378                    // Like `next`, waiting for each message is safe mid-test; the taint on a
1379                    // forced `None` is unobservable because the test fails below.
1380                    if let Some(v) = self.try_next_impl().await {
1381                        out.extend([v]);
1382                    } else {
1383                        return Err(format!(
1384                            "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1385                            i, n
1386                        ));
1387                    }
1388                }
1389                out.as_mut().sort();
1390                Ok(out)
1391            },
1392        }
1393    }
1394
1395    /// Collects all remaining messages from the external bincode stream into a collection,
1396    /// sorting them. This will wait until no more messages can possibly arrive.
1397    ///
1398    /// If this has to force pending nondeterministic work to run, it should be the last
1399    /// observation of the test; see [`collect`](SimReceiver::collect).
1400    pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self) -> C
1401    where
1402        T: Ord,
1403    {
1404        let mut collected = C::default();
1405        while let Some(v) = self.try_next_impl().await {
1406            collected.extend([v]);
1407        }
1408        collected.as_mut().sort();
1409        collected
1410    }
1411
1412    /// Asserts that the stream yields exactly the expected sequence of messages, in some order.
1413    /// This does not check that the stream ends, use [`Self::assert_yields_only_unordered`] for that.
1414    ///
1415    /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1416    pub fn assert_yields_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
1417        &self,
1418        expected: I,
1419    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1420    where
1421        T: Debug + PartialEq<T2>,
1422    {
1423        FutureTrackingCaller {
1424            future: async {
1425                let mut expected: Vec<T2> = expected.into_iter().collect();
1426
1427                while !expected.is_empty() {
1428                    // Like `next`, waiting for each expected message is safe mid-test; the
1429                    // taint on a forced `None` is unobservable because the test fails below.
1430                    if let Some(next) = self.try_next_impl().await {
1431                        let idx = expected.iter().enumerate().find(|(_, e)| &next == *e);
1432                        if let Some((i, _)) = idx {
1433                            expected.swap_remove(i);
1434                        } else {
1435                            return Err(format!("Stream yielded unexpected message: {:?}", next));
1436                        }
1437                    } else {
1438                        return Err(format!(
1439                            "Stream ended early, still expected: {:?}",
1440                            expected
1441                        ));
1442                    }
1443                }
1444
1445                Ok(())
1446            },
1447        }
1448    }
1449
1450    /// Asserts that the stream yields only the expected sequence of messages, in some order,
1451    /// and then ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1452    pub fn assert_yields_only_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
1453        &self,
1454        expected: I,
1455    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1456    where
1457        T: Debug + PartialEq<T2>,
1458    {
1459        ChainedFuture {
1460            first: self.assert_yields_unordered(expected),
1461            second: self.assert_no_more(),
1462            first_done: false,
1463        }
1464    }
1465}
1466
1467impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimSender<T, O, R> {
1468    fn with_sink<Out>(&self, thunk: impl FnOnce(&dyn Fn(T)) -> Out) -> Out {
1469        let (sender, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1470            let connections = connections.borrow();
1471            (
1472                connections
1473                    .input_senders
1474                    .get(connections.external_registered.get(&self.0).unwrap())
1475                    .unwrap()
1476                    .clone(),
1477                connections.quiescence.clone(),
1478            )
1479        });
1480
1481        thunk(&move |t| {
1482            sender
1483                .try_send(bincode::serialize(&t).unwrap().into())
1484                .unwrap();
1485            quiescence.resume();
1486        })
1487    }
1488}
1489
1490impl<T: Serialize + DeserializeOwned, O: Ordering> SimSender<T, O, ExactlyOnce> {
1491    /// Sends several messages to the external bincode sink. The messages will be asynchronously
1492    /// processed as part of the simulation, in non-deterministic order.
1493    pub fn send_many_unordered<I: IntoIterator<Item = T>>(&self, iter: I) {
1494        self.with_sink(|send| {
1495            for t in iter {
1496                send(t);
1497            }
1498        })
1499    }
1500}
1501
1502impl<T: Serialize + DeserializeOwned> SimSender<T, TotalOrder, ExactlyOnce> {
1503    /// Sends a message to the external bincode sink. The message will be asynchronously processed
1504    /// as part of the simulation.
1505    pub fn send(&self, t: T) {
1506        self.with_sink(|send| send(t));
1507    }
1508
1509    /// Sends several messages to the external bincode sink. The messages will be asynchronously
1510    /// processed as part of the simulation.
1511    pub fn send_many<I: IntoIterator<Item = T>>(&self, iter: I) {
1512        self.with_sink(|send| {
1513            for t in iter {
1514                send(t);
1515            }
1516        })
1517    }
1518}
1519
1520impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone
1521    for SimClusterReceiver<T, O, R>
1522{
1523    fn clone(&self) -> Self {
1524        *self
1525    }
1526}
1527
1528impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy
1529    for SimClusterReceiver<T, O, R>
1530{
1531}
1532
1533impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterReceiver<T, O, R> {
1534    fn member_connections(
1535        &self,
1536        member_id: u32,
1537    ) -> (Rc<Mutex<UnsyncReceiver<Bytes>>>, Rc<QuiescenceState>) {
1538        CURRENT_SIM_CONNECTIONS.with(|connections| {
1539            let connections = connections.borrow();
1540            let port = connections.external_registered.get(&self.0).unwrap();
1541            let receivers = connections.cluster_output_receivers.get(port).unwrap();
1542            (
1543                receivers[&member_id].clone(),
1544                connections.quiescence.clone(),
1545            )
1546        })
1547    }
1548
1549    /// See [`try_next_bytes`].
1550    async fn try_next_impl(&self, member_id: u32) -> Option<T> {
1551        let (receiver, quiescence) = self.member_connections(member_id);
1552        try_next_bytes(&receiver, &quiescence)
1553            .await
1554            .map(|bytes| bincode::deserialize(&bytes).unwrap())
1555    }
1556}
1557
1558impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, TotalOrder, ExactlyOnce> {
1559    /// Receives the next value from a specific cluster member, waiting (and letting the
1560    /// scheduler run any pending simulation work) until one is available. If the simulation
1561    /// becomes quiescent without producing a value, the test fails.
1562    ///
1563    /// This is safe to use in the middle of a test; to observe the *absence* of a value,
1564    /// use [`Self::try_next`].
1565    pub fn next(&self, member_id: u32) -> impl use<'_, T> + Future<Output = T> {
1566        // See `SimReceiver::next` for why waiting for a value never "overruns" the
1567        // simulation.
1568        FutureTrackingCaller {
1569            future: async move {
1570                self.try_next_impl(member_id).await.ok_or_else(|| {
1571                    "Stream ended (simulation quiescent), but another message was expected"
1572                        .to_owned()
1573                })
1574            },
1575        }
1576    }
1577
1578    /// Receives the next value from a specific cluster member, or returns `None` if no more
1579    /// values can possibly arrive.
1580    ///
1581    /// If answering requires forcing pending nondeterministic work to run, then afterwards,
1582    /// sending more input and then attempting to receive output will panic. Prefer
1583    /// [`Self::next`] when possible.
1584    pub async fn try_next(&self, member_id: u32) -> Option<T> {
1585        self.try_next_impl(member_id).await
1586    }
1587
1588    /// Collects all remaining values from a specific cluster member into a collection,
1589    /// waiting until no more values can possibly arrive.
1590    ///
1591    /// If this has to force pending nondeterministic work to run, it should be the last
1592    /// observation of the test; see [`SimReceiver::collect`].
1593    pub async fn collect<C: Default + Extend<T>>(self, member_id: u32) -> C {
1594        let mut out = C::default();
1595        while let Some(v) = self.try_next_impl(member_id).await {
1596            out.extend([v]);
1597        }
1598        out
1599    }
1600}
1601
1602impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, NoOrder, ExactlyOnce> {
1603    /// Receives the next `n` values from a specific cluster member, sorted, waiting (and
1604    /// letting the scheduler run any pending simulation work) until they are available. If
1605    /// the simulation becomes quiescent before `n` values arrive, the test fails.
1606    ///
1607    /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1608    pub fn collect_n_sorted<C: Default + Extend<T> + AsMut<[T]>>(
1609        &self,
1610        member_id: u32,
1611        n: usize,
1612    ) -> impl use<'_, T, C> + Future<Output = C>
1613    where
1614        T: Ord,
1615    {
1616        FutureTrackingCaller {
1617            future: async move {
1618                let mut out = C::default();
1619                for i in 0..n {
1620                    // Like `SimReceiver::next`, waiting for each message is safe mid-test;
1621                    // the taint on a forced `None` is unobservable because the test fails
1622                    // below.
1623                    if let Some(v) = self.try_next_impl(member_id).await {
1624                        out.extend([v]);
1625                    } else {
1626                        return Err(format!(
1627                            "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1628                            i, n
1629                        ));
1630                    }
1631                }
1632                out.as_mut().sort();
1633                Ok(out)
1634            },
1635        }
1636    }
1637
1638    /// Collects all remaining values from a specific cluster member, sorted, waiting until no
1639    /// more values can possibly arrive.
1640    ///
1641    /// If this has to force pending nondeterministic work to run, it should be the last
1642    /// observation of the test; see [`SimReceiver::collect`].
1643    pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self, member_id: u32) -> C
1644    where
1645        T: Ord,
1646    {
1647        let mut collected = C::default();
1648        while let Some(v) = self.try_next_impl(member_id).await {
1649            collected.extend([v]);
1650        }
1651        collected.as_mut().sort();
1652        collected
1653    }
1654}
1655
1656impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterSender<T, O, R> {
1657    fn with_sink<Out>(&self, thunk: impl FnOnce(&dyn Fn(u32, T)) -> Out) -> Out {
1658        let (senders, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1659            let connections = connections.borrow();
1660            (
1661                connections
1662                    .cluster_input_senders
1663                    .get(connections.external_registered.get(&self.0).unwrap())
1664                    .unwrap()
1665                    .clone(),
1666                connections.quiescence.clone(),
1667            )
1668        });
1669
1670        thunk(&move |member_id: u32, t: T| {
1671            let payload = bincode::serialize(&t).unwrap();
1672            senders[&member_id].try_send(Bytes::from(payload)).unwrap();
1673            quiescence.resume();
1674        })
1675    }
1676}
1677
1678impl<T: Serialize + DeserializeOwned, O: Ordering> SimClusterSender<T, O, ExactlyOnce> {
1679    /// Sends multiple values to specific cluster members. The messages will be asynchronously
1680    /// processed as part of the simulation, in non-deterministic order.
1681    pub fn send_many_unordered<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1682        self.with_sink(|send| {
1683            for (member_id, t) in iter {
1684                send(member_id, t);
1685            }
1686        })
1687    }
1688}
1689
1690impl<T: Serialize + DeserializeOwned> SimClusterSender<T, TotalOrder, ExactlyOnce> {
1691    /// Sends a value to a specific cluster member.
1692    pub fn send(&self, member_id: u32, t: T) {
1693        self.with_sink(|send| send(member_id, t));
1694    }
1695
1696    /// Sends multiple values to specific cluster members.
1697    pub fn send_many<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1698        self.with_sink(|send| {
1699            for (member_id, t) in iter {
1700                send(member_id, t);
1701            }
1702        })
1703    }
1704}
1705
1706enum LogKind<W: std::io::Write> {
1707    Null,
1708    Stderr,
1709    Custom(W),
1710}
1711
1712// via https://www.reddit.com/r/rust/comments/t69sld/is_there_a_way_to_allow_either_stdfmtwrite_or/
1713impl<W: std::io::Write> std::fmt::Write for LogKind<W> {
1714    fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
1715        match self {
1716            LogKind::Null => Ok(()),
1717            LogKind::Stderr => {
1718                eprint!("{}", s);
1719                Ok(())
1720            }
1721            LogKind::Custom(w) => w.write_all(s.as_bytes()).map_err(|_| std::fmt::Error),
1722        }
1723    }
1724}
1725
1726/// A running simulation, which manages the async DFIRs, tick DFIRs, and hook-based
1727/// scheduling decisions for non-deterministic operators like `batch` and `assume_ordering`.
1728///
1729/// The scheduler loops between three kinds of work:
1730/// - **Async DFIRs**: long-running top-level dataflows (one per process/cluster member) that
1731///   produce data consumed by ticks and observations.
1732/// - **Ticks**: tick-scoped DFIRs that execute a single tick. Before running, their associated
1733///   hooks (e.g. from `batch`) are resolved to decide what data to release into the tick.
1734/// - **Observations**: top-level locations that have hooks (e.g. from `assume_ordering` on a
1735///   non-tick stream) needing decisions, but no tick DFIR to execute. The scheduler just
1736///   resolves their hooks.
1737struct LaunchedSim<W: std::io::Write> {
1738    /// Top-level async DFIRs, one per process/cluster member. These run continuously and
1739    /// produce data that feeds into ticks and observations.
1740    async_dfirs: Vec<(LocationId, Option<u32>, DfirErased)>,
1741    /// Tick DFIRs whose parent async DFIR has made progress, so they may be ready to run.
1742    /// The scheduler further filters these by checking whether their hooks have pending decisions.
1743    possibly_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1744    /// Tick DFIRs whose parent async DFIR has not yet made progress since they were last checked.
1745    not_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1746    /// Top-level locations whose async DFIR has made progress and whose hooks (from top-level
1747    /// `assume_ordering`) may have ordering decisions to resolve. Unlike ticks, these have no
1748    /// DFIR to execute — only hook resolution.
1749    possibly_ready_observation: Vec<(LocationId, Option<u32>)>,
1750    /// Top-level locations whose async DFIR has not yet made progress since they were last checked.
1751    not_ready_observation: Vec<(LocationId, Option<u32>)>,
1752    /// Hooks keyed by (location, cluster_member_id). These are resolved *before* a tick runs
1753    /// (for `batch` hooks) or standalone (for top-level `assume_ordering` hooks via observations).
1754    hooks: Hooks<LocationId>,
1755    /// Inline hooks keyed by (tick location, cluster_member_id). These are resolved *during*
1756    /// tick execution via a `tokio::select!` loop, for operators like `assume_ordering` inside
1757    /// a tick that block on ordering decisions while the tick DFIR is running.
1758    inline_hooks: InlineHooks<LocationId>,
1759    log: LogKind<W>,
1760    /// Represents quiescence state of the simulation.
1761    quiescence: Rc<QuiescenceState>,
1762}
1763
1764impl<W: std::io::Write> LaunchedSim<W> {
1765    async fn scheduler(&mut self) {
1766        loop {
1767            tokio::task::yield_now().await;
1768            let mut any_made_progress = false;
1769            for (loc, c_id, dfir) in &mut self.async_dfirs {
1770                if dfir.run_tick().await {
1771                    any_made_progress = true;
1772                    let (now_ready, still_not_ready): (Vec<_>, Vec<_>) = self
1773                        .not_ready_ticks
1774                        .drain(..)
1775                        .partition(|(tick_loc, tick_c_id, _)| {
1776                            let LocationId::Tick(_, outer) = tick_loc else {
1777                                unreachable!()
1778                            };
1779                            outer.as_ref() == loc && tick_c_id == c_id
1780                        });
1781
1782                    self.possibly_ready_ticks.extend(now_ready);
1783                    self.not_ready_ticks.extend(still_not_ready);
1784
1785                    let (now_ready_obs, still_not_ready_obs): (Vec<_>, Vec<_>) = self
1786                        .not_ready_observation
1787                        .drain(..)
1788                        .partition(|(obs_loc, obs_c_id)| obs_loc == loc && obs_c_id == c_id);
1789
1790                    self.possibly_ready_observation.extend(now_ready_obs);
1791                    self.not_ready_observation.extend(still_not_ready_obs);
1792                }
1793            }
1794
1795            if any_made_progress {
1796                continue;
1797            } else {
1798                use bolero::generator::*;
1799
1800                let (ready_tick, mut not_ready_tick): (Vec<_>, Vec<_>) = self
1801                    .possibly_ready_ticks
1802                    .drain(..)
1803                    .partition(|(name, cid, _)| {
1804                        let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1805                        // All hooks must be ready (have received input or have a last value)
1806                        hooks.iter().all(|hook| hook.is_ready())
1807                            // And at least one hook must be able to make progress
1808                            && hooks.iter().any(|hook| {
1809                                hook.current_decision().unwrap_or(false)
1810                                    || hook.can_make_nontrivial_decision()
1811                            })
1812                    });
1813
1814                self.possibly_ready_ticks = ready_tick;
1815                self.not_ready_ticks.append(&mut not_ready_tick);
1816
1817                let (ready_obs, mut not_ready_obs): (Vec<_>, Vec<_>) = self
1818                    .possibly_ready_observation
1819                    .drain(..)
1820                    .partition(|(name, cid)| {
1821                        self.hooks
1822                            .get(&(name.clone(), *cid))
1823                            .into_iter()
1824                            .flatten()
1825                            .any(|hook| {
1826                                hook.current_decision().unwrap_or(false)
1827                                    || hook.can_make_nontrivial_decision()
1828                            })
1829                    });
1830
1831                self.possibly_ready_observation = ready_obs;
1832                self.not_ready_observation.append(&mut not_ready_obs);
1833
1834                if self.possibly_ready_ticks.is_empty()
1835                    && self.possibly_ready_observation.is_empty()
1836                {
1837                    // If any tick is blocked because a hook is not ready, that's a
1838                    // simulator bug — it means a singleton never received a value.
1839                    for (name, cid, _) in &self.not_ready_ticks {
1840                        let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1841                        abort_assert!(
1842                            hooks.iter().all(|hook| hook.is_ready()),
1843                            "tick has a hook that never became ready"
1844                        );
1845                    }
1846
1847                    // Signal quiescence and wait for new input.
1848                    self.quiescence.wait_for_resume().await;
1849                } else if self.quiescence.pause_nondet.get() > 0 {
1850                    // The test is querying whether the simulation can quiesce without
1851                    // nondeterministic work (see `QuiescenceCheckFuture`). Report that
1852                    // ticks/observations are pending and pause until the test decides how
1853                    // to proceed.
1854                    self.quiescence.nondet_pending.set(true);
1855                    self.quiescence.wake_settled();
1856                    self.quiescence.resume_notify.notified().await;
1857                    self.quiescence.nondet_pending.set(false);
1858                } else {
1859                    let next_tick_or_obs = (0..(self.possibly_ready_ticks.len()
1860                        + self.possibly_ready_observation.len()))
1861                        .any();
1862
1863                    if next_tick_or_obs < self.possibly_ready_ticks.len() {
1864                        let next_tick = next_tick_or_obs;
1865                        let mut removed = self.possibly_ready_ticks.remove(next_tick);
1866
1867                        match &mut self.log {
1868                            LogKind::Null => {}
1869                            LogKind::Stderr => {
1870                                if let Some(cid) = &removed.1 {
1871                                    eprintln!(
1872                                        "\n{}",
1873                                        format!("Running Tick (Cluster Member {})", cid)
1874                                            .color(colored::Color::Magenta)
1875                                            .bold()
1876                                    )
1877                                } else {
1878                                    eprintln!(
1879                                        "\n{}",
1880                                        "Running Tick".color(colored::Color::Magenta).bold()
1881                                    )
1882                                }
1883                            }
1884                            LogKind::Custom(writer) => {
1885                                writeln!(
1886                                    writer,
1887                                    "\n{}",
1888                                    "Running Tick".color(colored::Color::Magenta).bold()
1889                                )
1890                                .unwrap();
1891                            }
1892                        }
1893
1894                        let mut asterisk_indenter = |_line_no, write: &mut dyn std::fmt::Write| {
1895                            write.write_str(&"*".color(colored::Color::Magenta).bold())?;
1896                            write.write_str(" ")
1897                        };
1898
1899                        let mut tick_decision_writer =
1900                            (!matches!(self.log, LogKind::Null)).then(|| {
1901                                indenter::indented(&mut self.log).with_format(
1902                                    indenter::Format::Custom {
1903                                        inserter: &mut asterisk_indenter,
1904                                    },
1905                                )
1906                            });
1907
1908                        let hooks = self.hooks.get_mut(&(removed.0.clone(), removed.1)).unwrap();
1909                        run_hooks(tick_decision_writer.as_mut(), hooks);
1910
1911                        let run_tick_future = removed.2.run_tick();
1912                        if let Some(inline_hooks) =
1913                            self.inline_hooks.get_mut(&(removed.0.clone(), removed.1))
1914                        {
1915                            let mut run_tick_future_pinned = pin!(run_tick_future);
1916
1917                            loop {
1918                                tokio::select! {
1919                                    biased;
1920                                    r = &mut run_tick_future_pinned => {
1921                                        abort_assert!(r, "tick DFIR run_tick() returned false");
1922                                        break;
1923                                    }
1924                                    _ = async {} => {
1925                                        bolero_generator::any::scope::borrow_with(|driver| {
1926                                            for hook in inline_hooks.iter_mut() {
1927                                                if hook.pending_decision() {
1928                                                    if !hook.has_decision() {
1929                                                        hook.autonomous_decision(driver);
1930                                                    }
1931
1932                                                    hook.release_decision(
1933                                                        tick_decision_writer
1934                                                            .as_mut()
1935                                                            .map(|w| w as &mut dyn std::fmt::Write),
1936                                                    );
1937                                                }
1938                                            }
1939                                        });
1940                                    }
1941                                }
1942                            }
1943                        } else {
1944                            abort_assert!(
1945                                run_tick_future.await,
1946                                "tick DFIR run_tick() returned false"
1947                            );
1948                        }
1949
1950                        self.possibly_ready_ticks.push(removed);
1951                    } else {
1952                        let next_obs = next_tick_or_obs - self.possibly_ready_ticks.len();
1953                        let mut default_hooks = vec![];
1954                        let hooks = self
1955                            .hooks
1956                            .get_mut(&self.possibly_ready_observation[next_obs])
1957                            .unwrap_or(&mut default_hooks);
1958
1959                        let log_writer =
1960                            (!matches!(self.log, LogKind::Null)).then_some(&mut self.log);
1961                        run_hooks(log_writer, hooks);
1962                    }
1963                }
1964            }
1965        }
1966    }
1967}
1968
1969fn run_hooks<W: std::fmt::Write>(
1970    mut tick_decision_writer: Option<&mut W>,
1971    hooks: &mut Vec<Box<dyn SimHook>>,
1972) {
1973    let mut remaining_decision_count = hooks.len();
1974    let mut made_nontrivial_decision = false;
1975
1976    bolero::generator::bolero_generator::any::scope::borrow_with(|driver| {
1977        // first, scan manual decisions
1978        hooks.iter_mut().for_each(|hook| {
1979            if let Some(is_nontrivial) = hook.current_decision() {
1980                made_nontrivial_decision |= is_nontrivial;
1981                remaining_decision_count -= 1;
1982            } else if !hook.can_make_nontrivial_decision() {
1983                // if no nontrivial decision is possible, make a trivial one
1984                // (we need to do this in the first pass to force nontrivial decisions
1985                // on the remaining hooks)
1986                hook.autonomous_decision(driver, false);
1987                remaining_decision_count -= 1;
1988            }
1989        });
1990
1991        hooks.iter_mut().for_each(|hook| {
1992            if hook.current_decision().is_none() {
1993                made_nontrivial_decision |= hook.autonomous_decision(
1994                    driver,
1995                    !made_nontrivial_decision && remaining_decision_count == 1,
1996                );
1997                remaining_decision_count -= 1;
1998            }
1999
2000            hook.release_decision(
2001                tick_decision_writer
2002                    .as_deref_mut()
2003                    .map(|w| w as &mut dyn std::fmt::Write),
2004            );
2005        });
2006    });
2007}