Skip to main content

hydro_lang/sim/
compiled.rs

1//! Interfaces for compiled Hydro simulators and concrete simulation instances.
2//!
3//! NOTE: This module runs inside bolero's `catch_unwind` scope, which silently
4//! swallows panics. Internal invariant checks should use `abort_assert!`
5//! rather than `panic!`/`assert!`.
6//!
7//! TODO(mingwei): Panics inside the tick DFIR (generated code in the dylib) are
8//! also caught by bolero's `catch_unwind`. Consider a mechanism to detect and
9//! propagate those as well.
10
11/// Like `assert!`, but calls `std::process::abort()` instead of `panic!()`.
12/// Use for internal invariants that must not be silently caught by bolero.
13macro_rules! abort_assert {
14    ($cond:expr, $($arg:tt)*) => {
15        if !$cond {
16            eprintln!("Simulator internal error: {}", format!($($arg)*));
17            std::process::abort();
18        }
19    };
20}
21
22use core::{fmt, panic};
23use std::cell::{Cell, RefCell};
24use std::collections::{HashMap, VecDeque};
25use std::fmt::Debug;
26use std::panic::RefUnwindSafe;
27use std::path::Path;
28use std::pin::{Pin, pin};
29use std::rc::Rc;
30use std::task::ready;
31
32use bytes::Bytes;
33use colored::Colorize;
34use dfir_rs::scheduled::context::DfirErased;
35use futures::{Stream, StreamExt};
36use libloading::Library;
37use serde::Serialize;
38use serde::de::DeserializeOwned;
39use tempfile::TempPath;
40use tokio::sync::mpsc::UnboundedSender;
41use tokio::sync::{Mutex, Notify};
42use tokio_stream::wrappers::UnboundedReceiverStream;
43
44use super::runtime::{Hooks, InlineHooks};
45use super::{SimClusterReceiver, SimClusterSender, SimReceiver, SimSender};
46use crate::compile::builder::ExternalPortId;
47use crate::live_collections::stream::{ExactlyOnce, NoOrder, Ordering, Retries, TotalOrder};
48use crate::location::dynamic::LocationId;
49use crate::sim::graph::{SimExternalPort, SimExternalPortRegistry};
50use crate::sim::runtime::SimHook;
51
52struct QuiescenceState {
53    /// Set to true when the scheduler reaches quiescence; reset to false when new input is sent.
54    quiescent: Cell<bool>,
55    /// Notified when the scheduler reaches quiescence (wakes receivers waiting for data).
56    quiescence_notify: Notify,
57    /// Notified when new input is sent, signaling the scheduler to resume.
58    resume_notify: Notify,
59}
60
61impl QuiescenceState {
62    /// Signal that new input has been sent, waking the scheduler if it was quiescent.
63    fn resume(&self) {
64        self.quiescent.set(false);
65        self.resume_notify.notify_waiters();
66    }
67
68    /// Whether the scheduler is currently quiescent (no more progress possible without input).
69    fn is_quiescent(&self) -> bool {
70        self.quiescent.get()
71    }
72
73    /// Returns a future that completes when the scheduler next reaches quiescence.
74    fn notified(&self) -> tokio::sync::futures::Notified<'_> {
75        self.quiescence_notify.notified()
76    }
77
78    /// Enter quiescence and wait for new input before continuing.
79    async fn wait_for_resume(&self) {
80        self.quiescent.set(true);
81        self.quiescence_notify.notify_waiters();
82        self.resume_notify.notified().await;
83        self.quiescent.set(false);
84    }
85}
86
87struct SimConnections {
88    input_senders: HashMap<SimExternalPort, Rc<UnboundedSender<Bytes>>>,
89    output_receivers: HashMap<SimExternalPort, Rc<Mutex<UnboundedReceiverStream<Bytes>>>>,
90    cluster_input_senders: HashMap<SimExternalPort, HashMap<u32, Rc<UnboundedSender<Bytes>>>>,
91    cluster_output_receivers:
92        HashMap<SimExternalPort, HashMap<u32, Rc<Mutex<UnboundedReceiverStream<Bytes>>>>>,
93    external_registered: HashMap<ExternalPortId, SimExternalPort>,
94    quiescence: Rc<QuiescenceState>,
95    log: bool,
96}
97
98/// Implementation detail of [`crate::sim::continue_if!`](crate::continue_if); do not call directly.
99///
100/// If `condition` is false, aborts the current simulation instance by panicking with a special
101/// payload ([`bolero::generator::bolero_generator::any::Error`]) that bolero recognizes as an
102/// "invalid input" marker: the instance is discarded (not treated as a test failure, and never
103/// recorded as a reproducer) and exploration moves on to the next instance. If logging is
104/// enabled for the current instance, the failed assumption is logged first.
105#[doc(hidden)]
106#[track_caller]
107pub fn continue_if_impl(condition: bool, message: fmt::Arguments<'_>) {
108    if condition {
109        return;
110    }
111
112    let log = CURRENT_SIM_CONNECTIONS
113        .try_with(|connections| connections.borrow().log)
114        .unwrap_or(true);
115    if log {
116        eprintln!(
117            "{}",
118            render_continue_if_failure(std::panic::Location::caller(), message)
119        );
120    }
121
122    // Panics with `bolero_generator::any::Error`, which bolero's engines treat as an invalid
123    // input rather than a test failure. Both this function and bolero's `assume` are
124    // `#[track_caller]`, so the recorded location is the user's `continue_if!` call site.
125    bolero::generator::bolero_generator::any::assume(false, "simulation assumption failed");
126}
127
128/// Renders the log message for a failed assumption, echoing the source line with a caret
129/// pointing at the `continue_if!` call site, in the same style as the other simulator logs.
130fn render_continue_if_failure(
131    location: &std::panic::Location<'_>,
132    message: fmt::Arguments<'_>,
133) -> String {
134    use std::fmt::Write;
135
136    // `Location::file()` is relative to the directory the crate was compiled from (e.g. the
137    // workspace root), which may not match the current working directory (e.g. the crate
138    // root when running `cargo test`), so walk up from the current directory to find it.
139    let source_line = std::env::current_dir()
140        .ok()
141        .and_then(|cwd| {
142            cwd.ancestors()
143                .find_map(|base| std::fs::read_to_string(base.join(location.file())).ok())
144        })
145        .and_then(|content| {
146            content
147                .lines()
148                .nth((location.line() as usize).saturating_sub(1))
149                .map(|line| line.to_owned())
150        })
151        .unwrap_or_default();
152
153    let caret_indent = " ".repeat((location.column() as usize).saturating_sub(1));
154
155    let mut out = String::new();
156    let _ = writeln!(
157        out,
158        "\n{}",
159        "Condition failed (discarding simulation instance):"
160            .color(colored::Color::Yellow)
161            .bold()
162    );
163    let _ = writeln!(out, "{} {}", "-->".color(colored::Color::Blue), location);
164    let _ = writeln!(out, " {}{}", "|".color(colored::Color::Blue), source_line);
165    let _ = write!(
166        out,
167        " {}{}{}",
168        "|".color(colored::Color::Blue),
169        caret_indent,
170        format!("^ {}", message).color(colored::Color::Yellow)
171    );
172    out
173}
174
175tokio::task_local! {
176    static CURRENT_SIM_CONNECTIONS: RefCell<SimConnections>;
177}
178
179/// A handle to a compiled Hydro simulation, which can be instantiated and run.
180pub struct CompiledSim {
181    pub(super) _path: TempPath,
182    pub(super) lib: Library,
183    pub(super) externals_port_registry: SimExternalPortRegistry,
184    pub(super) unit_test_fuzz_iterations: usize,
185}
186
187#[sealed::sealed]
188/// A trait implemented by closures that can instantiate a compiled simulation.
189///
190/// This is needed to ensure [`RefUnwindSafe`] so instances can be created during fuzzing.
191pub trait Instantiator<'a>: RefUnwindSafe + Fn() -> CompiledSimInstance<'a> {}
192#[sealed::sealed]
193impl<'a, T: RefUnwindSafe + Fn() -> CompiledSimInstance<'a>> Instantiator<'a> for T {}
194
195fn null_handler(_args: fmt::Arguments) {}
196
197fn println_handler(args: fmt::Arguments) {
198    println!("{}", args);
199}
200
201fn eprintln_handler(args: fmt::Arguments) {
202    eprintln!("{}", args);
203}
204
205/// Creates a simulation instance, returning:
206/// - A list of async DFIRs to run (all process / cluster logic outside a tick)
207/// - A list of tick DFIRs to run (where the &'static str is for the tick location id)
208/// - A mapping of hooks for non-deterministic decisions at tick-input boundaries
209/// - A mapping of inline hooks for non-deterministic decisions inside ticks
210type SimLoaded<'a> = libloading::Symbol<
211    'a,
212    unsafe extern "Rust" fn(
213        should_color: bool,
214        external_out: &mut HashMap<usize, UnboundedReceiverStream<Bytes>>,
215        external_in: &mut HashMap<usize, UnboundedSender<Bytes>>,
216        cluster_external_out: &mut HashMap<usize, HashMap<u32, UnboundedReceiverStream<Bytes>>>,
217        cluster_external_in: &mut HashMap<usize, HashMap<u32, UnboundedSender<Bytes>>>,
218        println_handler: fn(fmt::Arguments<'_>),
219        eprintln_handler: fn(fmt::Arguments<'_>),
220    ) -> (
221        Vec<(&'static str, Option<u32>, DfirErased)>,
222        Vec<(&'static str, Option<u32>, DfirErased)>,
223        Hooks<&'static str>,
224        InlineHooks<&'static str>,
225    ),
226>;
227
228impl CompiledSim {
229    /// Executes the given closure with a single instance of the compiled simulation.
230    pub fn with_instance<T>(&self, thunk: impl FnOnce(CompiledSimInstance) -> T) -> T {
231        self.with_instantiator(|instantiator| thunk(instantiator()), true)
232    }
233
234    /// Executes the given closure with an [`Instantiator`], which can be called to create
235    /// independent instances of the simulation. This is useful for fuzzing, where we need to
236    /// re-execute the simulation several times with different decisions.
237    ///
238    /// The `always_log` parameter controls whether to log tick executions and stream releases. If
239    /// it is `true`, logging will always be enabled. If it is `false`, logging will only be
240    /// enabled if the `HYDRO_SIM_LOG` environment variable is set to `1`.
241    pub fn with_instantiator<T>(
242        &self,
243        thunk: impl FnOnce(&dyn Instantiator) -> T,
244        always_log: bool,
245    ) -> T {
246        let func: SimLoaded = unsafe { self.lib.get(b"__hydro_runtime").unwrap() };
247        let log = always_log || std::env::var("HYDRO_SIM_LOG").is_ok_and(|v| v == "1");
248        thunk(
249            &(|| CompiledSimInstance {
250                func: func.clone(),
251                externals_port_registry: self.externals_port_registry.clone(),
252                dylib_result: None,
253                log,
254            }),
255        )
256    }
257
258    /// Uses a fuzzing strategy to explore possible executions of the simulation. The provided
259    /// closure will be repeatedly executed with instances of the Hydro program where the
260    /// batching boundaries, order of messages, and retries are varied.
261    ///
262    /// During development, you should run the test that invokes this function with the `cargo sim`
263    /// command, which will use `libfuzzer` to intelligently explore the execution space. If a
264    /// failure is found, a minimized test case will be produced in a `sim-failures` directory.
265    /// When running the test with `cargo test` (such as in CI), if a reproducer is found it will
266    /// be executed, and if no reproducer is found a small number of random executions will be
267    /// performed.
268    pub fn fuzz(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) {
269        let caller_fn = crate::compile::ir::backtrace::Backtrace::get_backtrace(0)
270            .elements()
271            .into_iter()
272            .find(|e| {
273                !e.fn_name.starts_with("hydro_lang::sim::compiled")
274                    && !e.fn_name.starts_with("hydro_lang::sim::flow")
275                    && !e.fn_name.starts_with("fuzz<")
276                    && !e.fn_name.starts_with("<hydro_lang::sim")
277            })
278            .unwrap();
279
280        let caller_path = Path::new(&caller_fn.filename.unwrap()).to_path_buf();
281        let repro_folder = caller_path.parent().unwrap().join("sim-failures");
282
283        let caller_fuzz_repro_path = repro_folder
284            .join(caller_fn.fn_name.replace("::", "__"))
285            .with_extension("bin");
286
287        if std::env::var("BOLERO_FUZZER").is_ok() {
288            let corpus_dir = std::env::current_dir().unwrap().join(".fuzz-corpus");
289            std::fs::create_dir_all(&corpus_dir).unwrap();
290            let libfuzzer_args = format!(
291                "{} {} -artifact_prefix={}/ -handle_abrt=0",
292                corpus_dir.to_str().unwrap(),
293                corpus_dir.to_str().unwrap(),
294                corpus_dir.to_str().unwrap(),
295            );
296
297            std::fs::create_dir_all(&repro_folder).unwrap();
298
299            if !std::env::var("HYDRO_NO_FAILURE_OUTPUT").is_ok_and(|v| v == "1") {
300                unsafe {
301                    std::env::set_var(
302                        "BOLERO_FAILURE_OUTPUT",
303                        caller_fuzz_repro_path.to_str().unwrap(),
304                    );
305                }
306            }
307
308            unsafe {
309                std::env::set_var("BOLERO_LIBFUZZER_ARGS", libfuzzer_args);
310            }
311
312            self.with_instantiator(
313                |instantiator| {
314                    bolero::test(bolero::TargetLocation {
315                        package_name: "",
316                        manifest_dir: "",
317                        module_path: "",
318                        file: "",
319                        line: 0,
320                        item_path: "<unknown>::__bolero_item_path__",
321                        test_name: None,
322                    })
323                    .run_with_replay(move |is_replay| {
324                        let mut instance = instantiator();
325
326                        if instance.log {
327                            eprintln!(
328                                "{}",
329                                "\n==== New Simulation Instance ===="
330                                    .color(colored::Color::Cyan)
331                                    .bold()
332                            );
333                        }
334
335                        if is_replay {
336                            instance.log = true;
337                        }
338
339                        tokio::runtime::Builder::new_current_thread()
340                            .build()
341                            .unwrap()
342                            .block_on(async { instance.run(&mut thunk).await })
343                    })
344                },
345                false,
346            );
347        } else if let Ok(existing_bytes) = std::fs::read(&caller_fuzz_repro_path) {
348            self.fuzz_repro(existing_bytes, async |compiled| {
349                compiled.launch();
350                thunk().await
351            });
352        } else {
353            eprintln!(
354                "Running a fuzz test without `cargo sim` and no reproducer found at {}, using {} iterations with random inputs.",
355                caller_fuzz_repro_path.display(),
356                self.unit_test_fuzz_iterations,
357            );
358            self.with_instantiator(
359                |instantiator| {
360                    bolero::test(bolero::TargetLocation {
361                        package_name: "",
362                        manifest_dir: "",
363                        module_path: "",
364                        file: ".",
365                        line: 0,
366                        item_path: "<unknown>::__bolero_item_path__",
367                        test_name: None,
368                    })
369                    .with_iterations(self.unit_test_fuzz_iterations)
370                    .run(move || {
371                        let instance = instantiator();
372                        tokio::runtime::Builder::new_current_thread()
373                            .build()
374                            .unwrap()
375                            .block_on(async { instance.run(&mut thunk).await })
376                    })
377                },
378                false,
379            );
380        }
381    }
382
383    /// Executes the given closure with a single instance of the compiled simulation, using the
384    /// provided bytes as the source of fuzzing decisions. This can be used to manually reproduce a
385    /// failure found during fuzzing.
386    pub fn fuzz_repro<'a>(
387        &'a self,
388        bytes: Vec<u8>,
389        thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
390    ) {
391        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
392            self.with_instance(|instance| {
393                bolero::bolero_engine::any::scope::with(
394                    Box::new(bolero::bolero_engine::driver::object::Object(
395                        bolero::bolero_engine::driver::bytes::Driver::new(
396                            bytes,
397                            &Default::default(),
398                        ),
399                    )),
400                    || {
401                        tokio::runtime::Builder::new_current_thread()
402                            .build()
403                            .unwrap()
404                            .block_on(async { instance.run_without_launching(thunk).await })
405                    },
406                )
407            })
408        }));
409
410        if let Err(payload) = result {
411            if payload
412                .downcast_ref::<bolero::generator::bolero_generator::any::Error>()
413                .is_some()
414            {
415                // A `continue_if!` failed (or the driver ran out of entropy) while replaying the
416                // recorded bytes. Instances that fail an assumption are never recorded as
417                // failures, so this means the reproducer is stale or does not correspond to
418                // this program.
419                panic!(
420                    "simulation assumption failed while replaying recorded fuzz decisions; the reproducer may be stale or may not correspond to this program"
421                );
422            }
423            std::panic::resume_unwind(payload);
424        }
425    }
426
427    /// Exhaustively searches all possible executions of the simulation. The provided
428    /// closure will be repeatedly executed with instances of the Hydro program where the
429    /// batching boundaries, order of messages, and retries are varied.
430    ///
431    /// Exhaustive searching is feasible when the inputs to the Hydro program are finite and there
432    /// are no dataflow loops that generate infinite messages. Exhaustive searching provides a
433    /// stronger guarantee of correctness than fuzzing, but may take a long time to complete.
434    /// Because no fuzzer is involved, you can run exhaustive tests with `cargo test`.
435    ///
436    /// Returns the number of distinct executions explored.
437    pub fn exhaustive(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) -> usize {
438        if std::env::var("BOLERO_FUZZER").is_ok() {
439            eprintln!(
440                "Cannot run exhaustive tests with a fuzzer. Please use `cargo test` instead of `cargo sim`."
441            );
442            std::process::abort();
443        }
444
445        let mut count = 0;
446        let count_mut = &mut count;
447
448        let _span = tracing::debug_span!(target: "hydro_build", "sim_exhaustive").entered();
449
450        self.with_instantiator(
451            |instantiator| {
452                bolero::test(bolero::TargetLocation {
453                    package_name: "",
454                    manifest_dir: "",
455                    module_path: "",
456                    file: "",
457                    line: 0,
458                    item_path: "<unknown>::__bolero_item_path__",
459                    test_name: None,
460                })
461                .exhaustive()
462                .run_with_replay(move |is_replay| {
463                    *count_mut += 1;
464
465                    let mut instance = instantiator();
466                    if instance.log {
467                        eprintln!(
468                            "{}",
469                            "\n==== New Simulation Instance ===="
470                                .color(colored::Color::Cyan)
471                                .bold()
472                        );
473                    }
474
475                    if is_replay {
476                        instance.log = true;
477                    }
478
479                    tokio::runtime::Builder::new_current_thread()
480                        .build()
481                        .unwrap()
482                        .block_on(async { instance.run(&mut thunk).await })
483                })
484            },
485            false,
486        );
487
488        count
489    }
490}
491
492// This must be a tuple because it is referenced from generated code in `graph.rs`.
493type DylibResult = (
494    Vec<(&'static str, Option<u32>, DfirErased)>,
495    Vec<(&'static str, Option<u32>, DfirErased)>,
496    Hooks<&'static str>,
497    InlineHooks<&'static str>,
498);
499
500/// A single instance of a compiled Hydro simulation, which provides methods to interactively
501/// execute the simulation, feed inputs, and receive outputs.
502pub struct CompiledSimInstance<'a> {
503    func: SimLoaded<'a>,
504    externals_port_registry: SimExternalPortRegistry,
505    dylib_result: Option<DylibResult>,
506    log: bool,
507}
508
509impl<'a> CompiledSimInstance<'a> {
510    async fn run(self, thunk: impl AsyncFnOnce() + RefUnwindSafe) {
511        self.run_without_launching(async |instance| {
512            instance.launch();
513            thunk().await;
514        })
515        .await;
516    }
517
518    async fn run_without_launching(
519        mut self,
520        thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
521    ) {
522        let mut external_out: HashMap<usize, UnboundedReceiverStream<Bytes>> = HashMap::new();
523        let mut external_in: HashMap<usize, UnboundedSender<Bytes>> = HashMap::new();
524        let mut cluster_external_out: HashMap<usize, HashMap<u32, UnboundedReceiverStream<Bytes>>> =
525            HashMap::new();
526        let mut cluster_external_in: HashMap<usize, HashMap<u32, UnboundedSender<Bytes>>> =
527            HashMap::new();
528
529        let dylib_result = unsafe {
530            (self.func)(
531                colored::control::SHOULD_COLORIZE.should_colorize(),
532                &mut external_out,
533                &mut external_in,
534                &mut cluster_external_out,
535                &mut cluster_external_in,
536                if self.log {
537                    println_handler
538                } else {
539                    null_handler
540                },
541                if self.log {
542                    eprintln_handler
543                } else {
544                    null_handler
545                },
546            )
547        };
548
549        let registered = &self.externals_port_registry.registered;
550
551        let quiescence = Rc::new(QuiescenceState {
552            quiescent: Cell::new(false),
553            quiescence_notify: Notify::new(),
554            resume_notify: Notify::new(),
555        });
556
557        let mut input_senders = HashMap::new();
558        let mut output_receivers = HashMap::new();
559        let mut cluster_input_senders = HashMap::new();
560        let mut cluster_output_receivers = HashMap::new();
561
562        #[expect(
563            clippy::disallowed_methods,
564            reason = "inserts into maps also unordered"
565        )]
566        for sim_port in registered.values() {
567            let usize_key = sim_port.into_inner();
568            if let Some(sender) = external_in.remove(&usize_key) {
569                input_senders.insert(*sim_port, Rc::new(sender));
570            }
571            if let Some(receiver) = external_out.remove(&usize_key) {
572                output_receivers.insert(*sim_port, Rc::new(Mutex::new(receiver)));
573            }
574            if let Some(senders) = cluster_external_in.remove(&usize_key) {
575                cluster_input_senders.insert(
576                    *sim_port,
577                    senders
578                        .into_iter()
579                        .map(|(member, s)| (member, Rc::new(s)))
580                        .collect(),
581                );
582            }
583            if let Some(receivers) = cluster_external_out.remove(&usize_key) {
584                cluster_output_receivers.insert(
585                    *sim_port,
586                    receivers
587                        .into_iter()
588                        .map(|(member, r)| (member, Rc::new(Mutex::new(r))))
589                        .collect(),
590                );
591            }
592        }
593
594        self.dylib_result = Some(dylib_result);
595
596        let local_set = tokio::task::LocalSet::new();
597        local_set
598            .run_until(CURRENT_SIM_CONNECTIONS.scope(
599                RefCell::new(SimConnections {
600                    input_senders,
601                    output_receivers,
602                    cluster_input_senders,
603                    cluster_output_receivers,
604                    external_registered: self.externals_port_registry.registered.clone(),
605                    quiescence: quiescence.clone(),
606                    log: self.log,
607                }),
608                async move {
609                    thunk(self).await;
610                },
611            ))
612            .await;
613    }
614
615    /// Launches the simulation, which will asynchronously simulate the Hydro program. This should
616    /// be invoked but before receiving any messages.
617    fn launch(self) {
618        tokio::task::spawn_local(self.schedule_with_maybe_logger::<std::io::Empty>(None));
619    }
620
621    /// Returns a future that schedules simulation with the given logger for reporting the
622    /// simulation trace.
623    pub fn schedule_with_logger<W: std::io::Write>(
624        self,
625        log_writer: W,
626    ) -> impl use<W> + Future<Output = ()> {
627        self.schedule_with_maybe_logger(Some(log_writer))
628    }
629
630    fn schedule_with_maybe_logger<W: std::io::Write>(
631        mut self,
632        log_override: Option<W>,
633    ) -> impl use<W> + Future<Output = ()> {
634        let (async_dfirs, tick_dfirs, hooks, inline_hooks) = self.dylib_result.take().unwrap();
635
636        let not_ready_observation = async_dfirs
637            .iter()
638            .map(|(lid, c_id, _)| (serde_json::from_str(lid).unwrap(), *c_id))
639            .collect();
640
641        let quiescence = CURRENT_SIM_CONNECTIONS.with(|connections| {
642            let connections = connections.borrow();
643            connections.quiescence.clone()
644        });
645
646        let mut launched = LaunchedSim {
647            async_dfirs: async_dfirs
648                .into_iter()
649                .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
650                .collect(),
651            possibly_ready_ticks: vec![],
652            not_ready_ticks: tick_dfirs
653                .into_iter()
654                .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
655                .collect(),
656            possibly_ready_observation: vec![],
657            not_ready_observation,
658            hooks: hooks
659                .into_iter()
660                .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
661                .collect(),
662            inline_hooks: inline_hooks
663                .into_iter()
664                .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
665                .collect(),
666            log: if self.log {
667                if let Some(w) = log_override {
668                    LogKind::Custom(w)
669                } else {
670                    LogKind::Stderr
671                }
672            } else {
673                LogKind::Null
674            },
675            quiescence,
676        };
677
678        async move { launched.scheduler().await }
679    }
680}
681
682impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone for SimReceiver<T, O, R> {
683    fn clone(&self) -> Self {
684        *self
685    }
686}
687
688impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy for SimReceiver<T, O, R> {}
689
690impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimReceiver<T, O, R> {
691    async fn with_stream<Out>(
692        &self,
693        thunk: impl AsyncFnOnce(&mut Pin<&mut dyn Stream<Item = T>>) -> Out,
694    ) -> Out {
695        let (receiver, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
696            let connections = connections.borrow();
697            let port = connections.external_registered.get(&self.0).unwrap();
698            (
699                connections.output_receivers.get(port).unwrap().clone(),
700                connections.quiescence.clone(),
701            )
702        });
703
704        let mut receiver_stream = receiver.lock().await;
705        let mut notified_fut = pin!(quiescence.notified());
706        let mut quiescence_aware = futures::stream::poll_fn(|cx| {
707            use std::task::Poll;
708            match receiver_stream.poll_next_unpin(cx) {
709                Poll::Ready(Some(bytes)) => {
710                    return Poll::Ready(Some(bincode::deserialize(&bytes).unwrap()));
711                }
712                Poll::Ready(None) => return Poll::Ready(None),
713                Poll::Pending => {}
714            }
715            if quiescence.is_quiescent() {
716                return Poll::Ready(None);
717            }
718            let () = ready!(notified_fut.as_mut().poll(cx));
719            notified_fut.set(quiescence.notified());
720            Poll::Ready(None)
721        });
722        thunk(&mut pin!(&mut quiescence_aware)).await
723    }
724
725    /// Asserts that the stream has ended and no more messages can possibly arrive.
726    pub fn assert_no_more(self) -> impl Future<Output = ()>
727    where
728        T: Debug,
729    {
730        FutureTrackingCaller {
731            future: async move {
732                self.with_stream(async |stream| {
733                    if let Some(next) = stream.next().await {
734                        return Err(format!(
735                            "Stream yielded unexpected message: {:?}, expected termination",
736                            next
737                        ));
738                    }
739                    Ok(())
740                })
741                .await
742            },
743        }
744    }
745}
746
747impl<T: Serialize + DeserializeOwned> SimReceiver<T, TotalOrder, ExactlyOnce> {
748    /// Receives the next message from the external bincode stream. This will wait until a message
749    /// is available, or return `None` if no more messages can possibly arrive.
750    pub async fn next(&self) -> Option<T> {
751        self.with_stream(async |stream| stream.next().await).await
752    }
753
754    /// Collects all remaining messages from the external bincode stream into a collection. This
755    /// will wait until no more messages can possibly arrive.
756    pub async fn collect<C: Default + Extend<T>>(self) -> C {
757        self.with_stream(async |stream| stream.collect().await)
758            .await
759    }
760
761    /// Asserts that the stream yields exactly the expected sequence of messages, in order.
762    /// This does not check that the stream ends, use [`Self::assert_yields_only`] for that.
763    pub fn assert_yields<T2: Debug, I: IntoIterator<Item = T2>>(
764        &self,
765        expected: I,
766    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
767    where
768        T: Debug + PartialEq<T2>,
769    {
770        FutureTrackingCaller {
771            future: async {
772                let mut expected: VecDeque<T2> = expected.into_iter().collect();
773
774                while !expected.is_empty() {
775                    if let Some(next) = self.next().await {
776                        let next_expected = expected.pop_front().unwrap();
777                        if next != next_expected {
778                            return Err(format!(
779                                "Stream yielded unexpected message: {:?}, expected: {:?}",
780                                next, next_expected
781                            ));
782                        }
783                    } else {
784                        return Err(format!(
785                            "Stream ended early, still expected: {:?}",
786                            expected
787                        ));
788                    }
789                }
790
791                Ok(())
792            },
793        }
794    }
795
796    /// Asserts that the stream yields only the expected sequence of messages, in order,
797    /// and then ends.
798    pub fn assert_yields_only<T2: Debug, I: IntoIterator<Item = T2>>(
799        &self,
800        expected: I,
801    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
802    where
803        T: Debug + PartialEq<T2>,
804    {
805        ChainedFuture {
806            first: self.assert_yields(expected),
807            second: self.assert_no_more(),
808            first_done: false,
809        }
810    }
811}
812
813pin_project_lite::pin_project! {
814    // A future that tracks the location of the `.await` call for better panic messages.
815    //
816    // `#[track_caller]` is important for us to create assertion methods because it makes
817    // the panic backtrace show up at that method (instead of inside the call tree within
818    // that method). This is e.g. what `Option::unwrap` uses. Unfortunately, `#[track_caller]`
819    // does not work correctly for async methods (or `dyn Future` either), so we have to
820    // create these concrete future types that (1) have `#[track_caller]` on their `poll()`
821    // method and (2) have the `panic!` triggered in their `poll()` method (or in a directly
822    // nested concrete future).
823    struct FutureTrackingCaller<F: Future<Output = Result<(), String>>> {
824        #[pin]
825        future: F,
826    }
827}
828
829impl<F: Future<Output = Result<(), String>>> Future for FutureTrackingCaller<F> {
830    type Output = ();
831
832    #[track_caller]
833    fn poll(
834        mut self: Pin<&mut Self>,
835        cx: &mut std::task::Context<'_>,
836    ) -> std::task::Poll<Self::Output> {
837        match ready!(self.as_mut().project().future.poll(cx)) {
838            Ok(()) => std::task::Poll::Ready(()),
839            Err(e) => panic!("{}", e),
840        }
841    }
842}
843
844pin_project_lite::pin_project! {
845    // A future that first awaits the first future, then the second, propagating caller info.
846    //
847    // See [`FutureTrackingCaller`] for context.
848    struct ChainedFuture<F1: Future<Output = ()>, F2: Future<Output = ()>> {
849        #[pin]
850        first: F1,
851        #[pin]
852        second: F2,
853        first_done: bool,
854    }
855}
856
857impl<F1: Future<Output = ()>, F2: Future<Output = ()>> Future for ChainedFuture<F1, F2> {
858    type Output = ();
859
860    #[track_caller]
861    fn poll(
862        mut self: Pin<&mut Self>,
863        cx: &mut std::task::Context<'_>,
864    ) -> std::task::Poll<Self::Output> {
865        if !self.first_done {
866            ready!(self.as_mut().project().first.poll(cx));
867            *self.as_mut().project().first_done = true;
868        }
869
870        self.as_mut().project().second.poll(cx)
871    }
872}
873
874impl<T: Serialize + DeserializeOwned> SimReceiver<T, NoOrder, ExactlyOnce> {
875    /// Collects all remaining messages from the external bincode stream into a collection,
876    /// sorting them. This will wait until no more messages can possibly arrive.
877    pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self) -> C
878    where
879        T: Ord,
880    {
881        self.with_stream(async |stream| {
882            let mut collected: C = stream.collect().await;
883            collected.as_mut().sort();
884            collected
885        })
886        .await
887    }
888
889    /// Asserts that the stream yields exactly the expected sequence of messages, in some order.
890    /// This does not check that the stream ends, use [`Self::assert_yields_only_unordered`] for that.
891    pub fn assert_yields_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
892        &self,
893        expected: I,
894    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
895    where
896        T: Debug + PartialEq<T2>,
897    {
898        FutureTrackingCaller {
899            future: async {
900                self.with_stream(async |stream| {
901                    let mut expected: Vec<T2> = expected.into_iter().collect();
902
903                    while !expected.is_empty() {
904                        if let Some(next) = stream.next().await {
905                            let idx = expected.iter().enumerate().find(|(_, e)| &next == *e);
906                            if let Some((i, _)) = idx {
907                                expected.swap_remove(i);
908                            } else {
909                                return Err(format!(
910                                    "Stream yielded unexpected message: {:?}",
911                                    next
912                                ));
913                            }
914                        } else {
915                            return Err(format!(
916                                "Stream ended early, still expected: {:?}",
917                                expected
918                            ));
919                        }
920                    }
921
922                    Ok(())
923                })
924                .await
925            },
926        }
927    }
928
929    /// Asserts that the stream yields only the expected sequence of messages, in some order,
930    /// and then ends.
931    pub fn assert_yields_only_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
932        &self,
933        expected: I,
934    ) -> impl use<'_, T, T2, I> + Future<Output = ()>
935    where
936        T: Debug + PartialEq<T2>,
937    {
938        ChainedFuture {
939            first: self.assert_yields_unordered(expected),
940            second: self.assert_no_more(),
941            first_done: false,
942        }
943    }
944}
945
946impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimSender<T, O, R> {
947    fn with_sink<Out>(
948        &self,
949        thunk: impl FnOnce(&dyn Fn(T) -> Result<(), tokio::sync::mpsc::error::SendError<Bytes>>) -> Out,
950    ) -> Out {
951        let (sender, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
952            let connections = connections.borrow();
953            (
954                connections
955                    .input_senders
956                    .get(connections.external_registered.get(&self.0).unwrap())
957                    .unwrap()
958                    .clone(),
959                connections.quiescence.clone(),
960            )
961        });
962
963        thunk(&move |t| {
964            let res = sender.send(bincode::serialize(&t).unwrap().into());
965            quiescence.resume();
966            res
967        })
968    }
969}
970
971impl<T: Serialize + DeserializeOwned, O: Ordering> SimSender<T, O, ExactlyOnce> {
972    /// Sends several messages to the external bincode sink. The messages will be asynchronously
973    /// processed as part of the simulation, in non-deterministic order.
974    pub fn send_many_unordered<I: IntoIterator<Item = T>>(&self, iter: I) {
975        self.with_sink(|send| {
976            for t in iter {
977                send(t).unwrap();
978            }
979        })
980    }
981}
982
983impl<T: Serialize + DeserializeOwned> SimSender<T, TotalOrder, ExactlyOnce> {
984    /// Sends a message to the external bincode sink. The message will be asynchronously processed
985    /// as part of the simulation.
986    pub fn send(&self, t: T) {
987        self.with_sink(|send| send(t)).unwrap();
988    }
989
990    /// Sends several messages to the external bincode sink. The messages will be asynchronously
991    /// processed as part of the simulation.
992    pub fn send_many<I: IntoIterator<Item = T>>(&self, iter: I) {
993        self.with_sink(|send| {
994            for t in iter {
995                send(t).unwrap();
996            }
997        })
998    }
999}
1000
1001impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone
1002    for SimClusterReceiver<T, O, R>
1003{
1004    fn clone(&self) -> Self {
1005        *self
1006    }
1007}
1008
1009impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy
1010    for SimClusterReceiver<T, O, R>
1011{
1012}
1013
1014impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterReceiver<T, O, R> {
1015    async fn with_member_stream<Out>(
1016        &self,
1017        member_id: u32,
1018        thunk: impl AsyncFnOnce(&mut Pin<&mut dyn Stream<Item = T>>) -> Out,
1019    ) -> Out {
1020        let (receiver, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1021            let connections = connections.borrow();
1022            let port = connections.external_registered.get(&self.0).unwrap();
1023            let receivers = connections.cluster_output_receivers.get(port).unwrap();
1024            (
1025                receivers[&member_id].clone(),
1026                connections.quiescence.clone(),
1027            )
1028        });
1029
1030        let mut lock = receiver.lock().await;
1031        let mut notified_fut = pin!(quiescence.notified());
1032        let mut quiescence_aware = futures::stream::poll_fn(|cx| {
1033            use std::task::Poll;
1034            match lock.poll_next_unpin(cx) {
1035                Poll::Ready(Some(bytes)) => {
1036                    return Poll::Ready(Some(bincode::deserialize(&bytes).unwrap()));
1037                }
1038                Poll::Ready(None) => return Poll::Ready(None),
1039                Poll::Pending => {}
1040            }
1041            if quiescence.is_quiescent() {
1042                return Poll::Ready(None);
1043            }
1044            let () = ready!(notified_fut.as_mut().poll(cx));
1045            notified_fut.set(quiescence.notified());
1046            Poll::Ready(None)
1047        });
1048        thunk(&mut pin!(&mut quiescence_aware)).await
1049    }
1050}
1051
1052impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, TotalOrder, ExactlyOnce> {
1053    /// Receives the next value from a specific cluster member.
1054    pub async fn next(&self, member_id: u32) -> Option<T> {
1055        self.with_member_stream(member_id, async |stream| stream.next().await)
1056            .await
1057    }
1058
1059    /// Collects all remaining values from a specific cluster member into a collection.
1060    pub async fn collect<C: Default + Extend<T>>(self, member_id: u32) -> C {
1061        self.with_member_stream(member_id, async |stream| stream.collect().await)
1062            .await
1063    }
1064}
1065
1066impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, NoOrder, ExactlyOnce> {
1067    /// Collects all remaining values from a specific cluster member, sorted.
1068    pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self, member_id: u32) -> C
1069    where
1070        T: Ord,
1071    {
1072        self.with_member_stream(member_id, async |stream| {
1073            let mut collected: C = stream.collect().await;
1074            collected.as_mut().sort();
1075            collected
1076        })
1077        .await
1078    }
1079}
1080
1081impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterSender<T, O, R> {
1082    fn with_sink<Out>(
1083        &self,
1084        thunk: impl FnOnce(
1085            &dyn Fn(u32, T) -> Result<(), tokio::sync::mpsc::error::SendError<Bytes>>,
1086        ) -> Out,
1087    ) -> Out {
1088        let (senders, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1089            let connections = connections.borrow();
1090            (
1091                connections
1092                    .cluster_input_senders
1093                    .get(connections.external_registered.get(&self.0).unwrap())
1094                    .unwrap()
1095                    .clone(),
1096                connections.quiescence.clone(),
1097            )
1098        });
1099
1100        thunk(&move |member_id: u32, t: T| {
1101            let payload = bincode::serialize(&t).unwrap();
1102            let res = senders[&member_id].send(Bytes::from(payload));
1103            quiescence.resume();
1104            res
1105        })
1106    }
1107}
1108
1109impl<T: Serialize + DeserializeOwned, O: Ordering> SimClusterSender<T, O, ExactlyOnce> {
1110    /// Sends multiple values to specific cluster members. The messages will be asynchronously
1111    /// processed as part of the simulation, in non-deterministic order.
1112    pub fn send_many_unordered<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1113        self.with_sink(|send| {
1114            for (member_id, t) in iter {
1115                send(member_id, t).unwrap();
1116            }
1117        })
1118    }
1119}
1120
1121impl<T: Serialize + DeserializeOwned> SimClusterSender<T, TotalOrder, ExactlyOnce> {
1122    /// Sends a value to a specific cluster member.
1123    pub fn send(&self, member_id: u32, t: T) {
1124        self.with_sink(|send| send(member_id, t)).unwrap();
1125    }
1126
1127    /// Sends multiple values to specific cluster members.
1128    pub fn send_many<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1129        self.with_sink(|send| {
1130            for (member_id, t) in iter {
1131                send(member_id, t).unwrap();
1132            }
1133        })
1134    }
1135}
1136
1137enum LogKind<W: std::io::Write> {
1138    Null,
1139    Stderr,
1140    Custom(W),
1141}
1142
1143// via https://www.reddit.com/r/rust/comments/t69sld/is_there_a_way_to_allow_either_stdfmtwrite_or/
1144impl<W: std::io::Write> std::fmt::Write for LogKind<W> {
1145    fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
1146        match self {
1147            LogKind::Null => Ok(()),
1148            LogKind::Stderr => {
1149                eprint!("{}", s);
1150                Ok(())
1151            }
1152            LogKind::Custom(w) => w.write_all(s.as_bytes()).map_err(|_| std::fmt::Error),
1153        }
1154    }
1155}
1156
1157/// A running simulation, which manages the async DFIRs, tick DFIRs, and hook-based
1158/// scheduling decisions for non-deterministic operators like `batch` and `assume_ordering`.
1159///
1160/// The scheduler loops between three kinds of work:
1161/// - **Async DFIRs**: long-running top-level dataflows (one per process/cluster member) that
1162///   produce data consumed by ticks and observations.
1163/// - **Ticks**: tick-scoped DFIRs that execute a single tick. Before running, their associated
1164///   hooks (e.g. from `batch`) are resolved to decide what data to release into the tick.
1165/// - **Observations**: top-level locations that have hooks (e.g. from `assume_ordering` on a
1166///   non-tick stream) needing decisions, but no tick DFIR to execute. The scheduler just
1167///   resolves their hooks.
1168struct LaunchedSim<W: std::io::Write> {
1169    /// Top-level async DFIRs, one per process/cluster member. These run continuously and
1170    /// produce data that feeds into ticks and observations.
1171    async_dfirs: Vec<(LocationId, Option<u32>, DfirErased)>,
1172    /// Tick DFIRs whose parent async DFIR has made progress, so they may be ready to run.
1173    /// The scheduler further filters these by checking whether their hooks have pending decisions.
1174    possibly_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1175    /// Tick DFIRs whose parent async DFIR has not yet made progress since they were last checked.
1176    not_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1177    /// Top-level locations whose async DFIR has made progress and whose hooks (from top-level
1178    /// `assume_ordering`) may have ordering decisions to resolve. Unlike ticks, these have no
1179    /// DFIR to execute — only hook resolution.
1180    possibly_ready_observation: Vec<(LocationId, Option<u32>)>,
1181    /// Top-level locations whose async DFIR has not yet made progress since they were last checked.
1182    not_ready_observation: Vec<(LocationId, Option<u32>)>,
1183    /// Hooks keyed by (location, cluster_member_id). These are resolved *before* a tick runs
1184    /// (for `batch` hooks) or standalone (for top-level `assume_ordering` hooks via observations).
1185    hooks: Hooks<LocationId>,
1186    /// Inline hooks keyed by (tick location, cluster_member_id). These are resolved *during*
1187    /// tick execution via a `tokio::select!` loop, for operators like `assume_ordering` inside
1188    /// a tick that block on ordering decisions while the tick DFIR is running.
1189    inline_hooks: InlineHooks<LocationId>,
1190    log: LogKind<W>,
1191    /// Represents quiescence state of the simulation.
1192    quiescence: Rc<QuiescenceState>,
1193}
1194
1195impl<W: std::io::Write> LaunchedSim<W> {
1196    async fn scheduler(&mut self) {
1197        loop {
1198            tokio::task::yield_now().await;
1199            let mut any_made_progress = false;
1200            for (loc, c_id, dfir) in &mut self.async_dfirs {
1201                if dfir.run_tick().await {
1202                    any_made_progress = true;
1203                    let (now_ready, still_not_ready): (Vec<_>, Vec<_>) = self
1204                        .not_ready_ticks
1205                        .drain(..)
1206                        .partition(|(tick_loc, tick_c_id, _)| {
1207                            let LocationId::Tick(_, outer) = tick_loc else {
1208                                unreachable!()
1209                            };
1210                            outer.as_ref() == loc && tick_c_id == c_id
1211                        });
1212
1213                    self.possibly_ready_ticks.extend(now_ready);
1214                    self.not_ready_ticks.extend(still_not_ready);
1215
1216                    let (now_ready_obs, still_not_ready_obs): (Vec<_>, Vec<_>) = self
1217                        .not_ready_observation
1218                        .drain(..)
1219                        .partition(|(obs_loc, obs_c_id)| obs_loc == loc && obs_c_id == c_id);
1220
1221                    self.possibly_ready_observation.extend(now_ready_obs);
1222                    self.not_ready_observation.extend(still_not_ready_obs);
1223                }
1224            }
1225
1226            if any_made_progress {
1227                continue;
1228            } else {
1229                use bolero::generator::*;
1230
1231                let (ready_tick, mut not_ready_tick): (Vec<_>, Vec<_>) = self
1232                    .possibly_ready_ticks
1233                    .drain(..)
1234                    .partition(|(name, cid, _)| {
1235                        let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1236                        // All hooks must be ready (have received input or have a last value)
1237                        hooks.iter().all(|hook| hook.is_ready())
1238                            // And at least one hook must be able to make progress
1239                            && hooks.iter().any(|hook| {
1240                                hook.current_decision().unwrap_or(false)
1241                                    || hook.can_make_nontrivial_decision()
1242                            })
1243                    });
1244
1245                self.possibly_ready_ticks = ready_tick;
1246                self.not_ready_ticks.append(&mut not_ready_tick);
1247
1248                let (ready_obs, mut not_ready_obs): (Vec<_>, Vec<_>) = self
1249                    .possibly_ready_observation
1250                    .drain(..)
1251                    .partition(|(name, cid)| {
1252                        self.hooks
1253                            .get(&(name.clone(), *cid))
1254                            .into_iter()
1255                            .flatten()
1256                            .any(|hook| {
1257                                hook.current_decision().unwrap_or(false)
1258                                    || hook.can_make_nontrivial_decision()
1259                            })
1260                    });
1261
1262                self.possibly_ready_observation = ready_obs;
1263                self.not_ready_observation.append(&mut not_ready_obs);
1264
1265                if self.possibly_ready_ticks.is_empty()
1266                    && self.possibly_ready_observation.is_empty()
1267                {
1268                    // If any tick is blocked because a hook is not ready, that's a
1269                    // simulator bug — it means a singleton never received a value.
1270                    for (name, cid, _) in &self.not_ready_ticks {
1271                        let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1272                        abort_assert!(
1273                            hooks.iter().all(|hook| hook.is_ready()),
1274                            "tick has a hook that never became ready"
1275                        );
1276                    }
1277
1278                    // Signal quiescence and wait for new input.
1279                    self.quiescence.wait_for_resume().await;
1280                } else {
1281                    let next_tick_or_obs = (0..(self.possibly_ready_ticks.len()
1282                        + self.possibly_ready_observation.len()))
1283                        .any();
1284
1285                    if next_tick_or_obs < self.possibly_ready_ticks.len() {
1286                        let next_tick = next_tick_or_obs;
1287                        let mut removed = self.possibly_ready_ticks.remove(next_tick);
1288
1289                        match &mut self.log {
1290                            LogKind::Null => {}
1291                            LogKind::Stderr => {
1292                                if let Some(cid) = &removed.1 {
1293                                    eprintln!(
1294                                        "\n{}",
1295                                        format!("Running Tick (Cluster Member {})", cid)
1296                                            .color(colored::Color::Magenta)
1297                                            .bold()
1298                                    )
1299                                } else {
1300                                    eprintln!(
1301                                        "\n{}",
1302                                        "Running Tick".color(colored::Color::Magenta).bold()
1303                                    )
1304                                }
1305                            }
1306                            LogKind::Custom(writer) => {
1307                                writeln!(
1308                                    writer,
1309                                    "\n{}",
1310                                    "Running Tick".color(colored::Color::Magenta).bold()
1311                                )
1312                                .unwrap();
1313                            }
1314                        }
1315
1316                        let mut asterisk_indenter = |_line_no, write: &mut dyn std::fmt::Write| {
1317                            write.write_str(&"*".color(colored::Color::Magenta).bold())?;
1318                            write.write_str(" ")
1319                        };
1320
1321                        let mut tick_decision_writer = indenter::indented(&mut self.log)
1322                            .with_format(indenter::Format::Custom {
1323                                inserter: &mut asterisk_indenter,
1324                            });
1325
1326                        let hooks = self.hooks.get_mut(&(removed.0.clone(), removed.1)).unwrap();
1327                        run_hooks(&mut tick_decision_writer, hooks);
1328
1329                        let run_tick_future = removed.2.run_tick();
1330                        if let Some(inline_hooks) =
1331                            self.inline_hooks.get_mut(&(removed.0.clone(), removed.1))
1332                        {
1333                            let mut run_tick_future_pinned = pin!(run_tick_future);
1334
1335                            loop {
1336                                tokio::select! {
1337                                    biased;
1338                                    r = &mut run_tick_future_pinned => {
1339                                        abort_assert!(r, "tick DFIR run_tick() returned false");
1340                                        break;
1341                                    }
1342                                    _ = async {} => {
1343                                        bolero_generator::any::scope::borrow_with(|driver| {
1344                                            for hook in inline_hooks.iter_mut() {
1345                                                if hook.pending_decision() {
1346                                                    if !hook.has_decision() {
1347                                                        hook.autonomous_decision(driver);
1348                                                    }
1349
1350                                                    hook.release_decision(&mut tick_decision_writer);
1351                                                }
1352                                            }
1353                                        });
1354                                    }
1355                                }
1356                            }
1357                        } else {
1358                            abort_assert!(
1359                                run_tick_future.await,
1360                                "tick DFIR run_tick() returned false"
1361                            );
1362                        }
1363
1364                        self.possibly_ready_ticks.push(removed);
1365                    } else {
1366                        let next_obs = next_tick_or_obs - self.possibly_ready_ticks.len();
1367                        let mut default_hooks = vec![];
1368                        let hooks = self
1369                            .hooks
1370                            .get_mut(&self.possibly_ready_observation[next_obs])
1371                            .unwrap_or(&mut default_hooks);
1372
1373                        run_hooks(&mut self.log, hooks);
1374                    }
1375                }
1376            }
1377        }
1378    }
1379}
1380
1381fn run_hooks(tick_decision_writer: &mut impl std::fmt::Write, hooks: &mut Vec<Box<dyn SimHook>>) {
1382    let mut remaining_decision_count = hooks.len();
1383    let mut made_nontrivial_decision = false;
1384
1385    bolero::generator::bolero_generator::any::scope::borrow_with(|driver| {
1386        // first, scan manual decisions
1387        hooks.iter_mut().for_each(|hook| {
1388            if let Some(is_nontrivial) = hook.current_decision() {
1389                made_nontrivial_decision |= is_nontrivial;
1390                remaining_decision_count -= 1;
1391            } else if !hook.can_make_nontrivial_decision() {
1392                // if no nontrivial decision is possible, make a trivial one
1393                // (we need to do this in the first pass to force nontrivial decisions
1394                // on the remaining hooks)
1395                hook.autonomous_decision(driver, false);
1396                remaining_decision_count -= 1;
1397            }
1398        });
1399
1400        hooks.iter_mut().for_each(|hook| {
1401            if hook.current_decision().is_none() {
1402                made_nontrivial_decision |= hook.autonomous_decision(
1403                    driver,
1404                    !made_nontrivial_decision && remaining_decision_count == 1,
1405                );
1406                remaining_decision_count -= 1;
1407            }
1408
1409            hook.release_decision(tick_decision_writer);
1410        });
1411    });
1412}