hydro_lang/sim_hooks.rs
1//! Handle types for **simulator hooks**: scripting the decisions of unsafe operators.
2//!
3//! Every unsafe operator (like [`Stream::batch`](crate::live_collections::Stream::batch) or
4//! [`Singleton::snapshot`](crate::live_collections::Singleton::snapshot)) takes a
5//! [`NonDet`](crate::nondet::NonDet) guard. A guard can optionally carry a **hook handle**,
6//! which lets a simulation test take manual control of the non-deterministic decision made
7//! by that operator (which elements form the next batch, which version of a piece of state
8//! a snapshot reveals, ...). See `hydro_lang::sim::hooks` for the test-side scripting API.
9//!
10//! Handles are created from the [`FlowBuilder`](crate::compile::builder::FlowBuilder) via
11//! [`FlowBuilder::sim_hook`](crate::compile::builder::FlowBuilder::sim_hook) *before* the
12//! program under test is constructed, and attached to the operator they control with the
13//! `nondet!(... hook = handle)` syntax. Handles are small and `Copy`: the same value is
14//! passed into the program during construction and used later inside the test body to
15//! script decisions.
16//!
17//! This module contains only the handle types themselves (plain data), so components can
18//! expose hookable signatures (e.g. `nondet_batch: NonDet<Option<BatchHook<u32>>>`, passed
19//! directly to the `batch` operator it controls) without pulling
20//! in any simulator machinery; binding a hook in a flow that is *deployed* rather than
21//! simulated is harmless metadata that non-simulator backends ignore.
22
23use std::marker::PhantomData;
24
25use crate::live_collections::boundedness::{Boundedness, Unbounded};
26use crate::live_collections::stream::{ExactlyOnce, Ordering, Retries, TotalOrder};
27
28/// A simulator hook handle (or a set of them) that can be created in one call to
29/// [`FlowBuilder::sim_hook`](crate::compile::builder::FlowBuilder::sim_hook).
30///
31/// Individual handle types implement this trait, and a struct of handles (a component's
32/// "testing interface") can implement it by creating every field. Fields are typed
33/// `Option<...>` so the struct doubles as a composite hook payload: its [`Default`]
34/// ("no hooks") is what a plain `nondet!(...)` guard carries, while `flow.sim_hook()`
35/// fills in every handle:
36///
37/// ```rust,ignore
38/// #[derive(Clone, Copy, Default)]
39/// pub struct CounterHooks {
40/// pub batch: Option<BatchHook<u32>>,
41/// pub snapshot: Option<SnapshotHook<u64>>,
42/// }
43///
44/// impl SimHook for CounterHooks {
45/// fn create(next_id: &mut dyn FnMut() -> usize) -> Self {
46/// CounterHooks {
47/// batch: SimHook::create(next_id),
48/// snapshot: SimHook::create(next_id),
49/// }
50/// }
51/// }
52/// ```
53///
54/// Such structs nest (a field can itself be a struct of handles), and since handles are
55/// `Copy` a test can pass the struct around or destructure it freely.
56pub trait SimHook {
57 /// Creates every handle in this value, allocating fresh IDs via `next_id`.
58 fn create(next_id: &mut dyn FnMut() -> usize) -> Self;
59}
60
61impl<H: SimHook> SimHook for Option<H> {
62 fn create(next_id: &mut dyn FnMut() -> usize) -> Self {
63 Some(H::create(next_id))
64 }
65}
66
67/// A hook handle controlling a `batch` operator over a stream of `T` elements with ordering
68/// `O` and retry guarantee `R` (mirroring the type of the stream being batched).
69///
70/// A decision for a batch hook says which buffered elements form the next batch released
71/// into the tick. See `hydro_lang::sim::hooks` for the decisions offered.
72pub struct BatchHook<T, O: Ordering = TotalOrder, R: Retries = ExactlyOnce> {
73 pub(crate) id: usize,
74 pub(crate) _phantom: PhantomData<fn(T, O, R)>,
75}
76
77impl<T, O: Ordering, R: Retries> Clone for BatchHook<T, O, R> {
78 fn clone(&self) -> Self {
79 *self
80 }
81}
82impl<T, O: Ordering, R: Retries> Copy for BatchHook<T, O, R> {}
83
84impl<T, O: Ordering, R: Retries> std::fmt::Debug for BatchHook<T, O, R> {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("BatchHook").field("id", &self.id).finish()
87 }
88}
89
90impl<T, O: Ordering, R: Retries> SimHook for BatchHook<T, O, R> {
91 fn create(next_id: &mut dyn FnMut() -> usize) -> Self {
92 BatchHook {
93 id: next_id(),
94 _phantom: PhantomData,
95 }
96 }
97}
98
99/// A hook handle controlling a `snapshot` operator over a singleton of `T`.
100///
101/// A decision for a snapshot hook picks which buffered version of the state the next tick
102/// execution observes. See `hydro_lang::sim::hooks` for the decisions offered.
103pub struct SnapshotHook<T> {
104 pub(crate) id: usize,
105 pub(crate) _phantom: PhantomData<fn(T)>,
106}
107
108impl<T> Clone for SnapshotHook<T> {
109 fn clone(&self) -> Self {
110 *self
111 }
112}
113impl<T> Copy for SnapshotHook<T> {}
114
115impl<T> std::fmt::Debug for SnapshotHook<T> {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct("SnapshotHook")
118 .field("id", &self.id)
119 .finish()
120 }
121}
122
123impl<T> SimHook for SnapshotHook<T> {
124 fn create(next_id: &mut dyn FnMut() -> usize) -> Self {
125 SnapshotHook {
126 id: next_id(),
127 _phantom: PhantomData,
128 }
129 }
130}
131
132/// A hook handle controlling an `assume_ordering` operator over `T` elements.
133///
134/// A top-level decision selects the next buffered element to release. An `assume_ordering`
135/// inside a tick instead takes one exhaustive ordering of that tick's complete input. See
136/// `hydro_lang::sim::hooks` for the decisions offered.
137pub struct OrderingHook<T, B: Boundedness = Unbounded> {
138 pub(crate) id: usize,
139 pub(crate) _phantom: PhantomData<fn(T, B)>,
140}
141
142impl<T, B: Boundedness> Clone for OrderingHook<T, B> {
143 fn clone(&self) -> Self {
144 *self
145 }
146}
147impl<T, B: Boundedness> Copy for OrderingHook<T, B> {}
148
149impl<T, B: Boundedness> std::fmt::Debug for OrderingHook<T, B> {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("OrderingHook")
152 .field("id", &self.id)
153 .finish()
154 }
155}
156
157impl<T, B: Boundedness> SimHook for OrderingHook<T, B> {
158 fn create(next_id: &mut dyn FnMut() -> usize) -> Self {
159 OrderingHook {
160 id: next_id(),
161 _phantom: PhantomData,
162 }
163 }
164}