dfir_rs/scheduled/context.rs
1//! Module for the inline DFIR runtime context and execution engine.
2//!
3//! Provides [`Context`] (the lightweight operator context) and
4//! [`Dfir`] (the dataflow execution wrapper).
5
6use std::future::Future;
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::rc::Rc;
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12use std::task::Wake;
13
14#[cfg(feature = "meta")]
15use dfir_lang::diagnostic::{Diagnostic, Diagnostics, SerdeSpan};
16#[cfg(feature = "meta")]
17use dfir_lang::graph::DfirGraph;
18
19use super::metrics::{DfirMetrics, DfirMetricsIntervals, MetricsState};
20use crate::scheduled::ticks::TickInstant;
21
22/// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
23/// (the external runner). Shared via `Arc` between both.
24///
25/// When external data arrives (e.g., a tokio stream receives a message), the [`Context::waker`]
26/// fires, which sets `can_start_tick` and wakes the [`Dfir::run`](Dfir::run) task so it starts a new tick.
27/// Implements [`Wake`] directly so it can be used as a `Waker` without an extra wrapper.
28#[doc(hidden)]
29pub struct WakeState {
30 /// Set to `true` when external data arrives, signaling that a new tick should run.
31 /// Checked by [`Dfir::run_tick`](Dfir::run_tick) and [`Dfir::run_available`](Dfir::run_available).
32 can_start_tick: std::sync::atomic::AtomicBool,
33 /// Wakes the [`Dfir::run`](Dfir::run) task from its idle `poll_fn` sleep.
34 task_waker: futures::task::AtomicWaker,
35}
36
37impl Default for WakeState {
38 fn default() -> Self {
39 Self {
40 can_start_tick: std::sync::atomic::AtomicBool::new(false),
41 task_waker: futures::task::AtomicWaker::new(),
42 }
43 }
44}
45
46impl Wake for WakeState {
47 fn wake(self: Arc<Self>) {
48 self.wake_by_ref();
49 }
50
51 fn wake_by_ref(self: &Arc<Self>) {
52 self.can_start_tick.store(true, Ordering::Relaxed);
53 self.task_waker.wake();
54 }
55}
56
57/// A lightweight context for inline codegen that avoids the overhead of the full
58/// scheduled graph (no tokio channels, no scheduler queues, no loop machinery).
59///
60/// Exposes methods that operator-generated code calls on both
61/// `df` (for prologues: `request_task`) and
62/// `context` (for iterators: `current_tick`, `schedule_subgraph`, etc.).
63///
64/// # Metrics typestate
65///
66/// The `M` type parameter is a [`MetricsState`] typestate which statically records whether this
67/// context tracks runtime metrics:
68///
69/// - `Context<DfirMetrics>` (the default) is created via [`Context::new_with_metrics`] and holds
70/// a shared `Rc<DfirMetrics>` handle, accessible via [`Context::metrics`].
71/// - `Context<()>` is created via [`Context::new`] and holds no metrics state whatsoever. The
72/// `metrics()` accessor does not exist on it, so it is a compile error to try to read metrics
73/// from a context which never tracked them.
74pub struct Context<M: MetricsState = DfirMetrics> {
75 /// Counter for number of ticks run.
76 current_tick: TickInstant,
77 /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
78 /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
79 wake_state: Arc<WakeState>,
80 /// Live-updating DFIR runtime metrics via interior mutability (`Rc<DfirMetrics>`), or `()`
81 /// when metrics are not tracked.
82 metrics: M::Handle,
83 /// Tasks buffered via [`Self::request_task`], spawned via [`tokio::spawn`] by
84 /// [`Dfir::spawn_tasks`] once the runtime is running inside a tokio runtime.
85 ///
86 /// Futures are required to be `Send` so that `Context` (and therefore [`Dfir`]) can be moved
87 /// between threads before being run, and so that the tasks can be spawned onto any tokio
88 /// runtime (no `LocalSet` required).
89 #[cfg(feature = "tokio")]
90 tasks_to_spawn: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
91}
92
93impl Context<()> {
94 /// Create a new inline context with a fresh wake state and **no** metrics tracking.
95 ///
96 /// The returned `Context<()>` statically contains no metrics state. Use
97 /// [`Context::new_with_metrics`] to create a `Context<DfirMetrics>` which tracks metrics.
98 pub fn new() -> Self {
99 Self::with_metrics_handle(())
100 }
101}
102
103impl Default for Context<()> {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl Context<DfirMetrics> {
110 /// Create a new inline context with a fresh wake state and the given (pre-populated) metrics.
111 pub fn new_with_metrics(metrics: DfirMetrics) -> Self {
112 Self::with_metrics_handle(Rc::new(metrics))
113 }
114
115 /// Returns a reference to the runtime metrics.
116 pub fn metrics(&self) -> &Rc<DfirMetrics> {
117 &self.metrics
118 }
119}
120
121impl<M: MetricsState> Context<M> {
122 fn with_metrics_handle(metrics: M::Handle) -> Self {
123 Self {
124 current_tick: TickInstant::default(),
125 wake_state: Arc::default(),
126 metrics,
127 #[cfg(feature = "tokio")]
128 tasks_to_spawn: Vec::new(),
129 }
130 }
131
132 // --- Methods called as `df.xxx()` in operator prologues ---
133
134 /// Buffers an async task to be spawned later by `Dfir::spawn_tasks`.
135 ///
136 /// Tasks are deferred because `write_prologue` runs during graph construction,
137 /// which may occur before a tokio runtime is entered. Buffered tasks are
138 /// drained and spawned via [`tokio::spawn`] at the start of
139 /// [`Dfir::run_tick`]. Tasks requested after that point remain buffered until
140 /// the next call to [`Dfir::run_tick`].
141 ///
142 /// The future must be `Send` so that the `Context` remains `Send` and so the task can be
143 /// spawned onto any tokio runtime.
144 #[cfg(feature = "tokio")]
145 pub fn request_task<Fut>(&mut self, future: Fut)
146 where
147 Fut: Future<Output = ()> + Send + 'static,
148 {
149 self.tasks_to_spawn.push(Box::pin(future));
150 }
151
152 // --- Methods called as `context.xxx()` in operator iterators ---
153
154 /// Gets the current tick count.
155 pub fn current_tick(&self) -> TickInstant {
156 self.current_tick
157 }
158
159 /// Signals that external data has arrived and a new tick should be started.
160 pub fn schedule_subgraph(&self, is_external: bool) {
161 if is_external {
162 self.wake_state.wake_by_ref();
163 }
164 }
165
166 /// Returns a waker that signals external data has arrived.
167 pub fn waker(&self) -> std::task::Waker {
168 std::task::Waker::from(self.wake_state.clone())
169 }
170
171 /// Increments the tick counter.
172 /// Called by the generated tick closure at the end of each tick.
173 #[doc(hidden)]
174 pub fn __end_tick(&mut self) {
175 self.current_tick += crate::scheduled::ticks::TickDuration::SINGLE_TICK;
176 }
177}
178
179/// An executable DFIR dataflow, as created by
180/// [`dfir_syntax!`](crate::dfir_syntax). Provides the [`Self::run`],
181/// [`Self::run_available`], and [`Self::run_tick`] family of methods to
182/// execute the dataflow.
183///
184/// # Design
185///
186/// The inline codegen generates an `async move |df: &mut Context<M>|` closure that captures
187/// dataflow-specific state (handoff buffers, source iterators) and receives the [`Context`]
188/// (tick counter, metrics) by reference each tick. `Dfir` owns both the
189/// closure and the context, and coordinates tick lifecycle and idle/wake behavior.
190///
191/// We use a single opaque closure rather than generating a bespoke struct per dataflow because:
192/// - The closure naturally captures exactly the state it needs with correct lifetimes
193/// - No codegen needed for struct definitions, field accessors, or initialization
194/// - Rust's async closure machinery handles the complex state machine (suspend/resume across
195/// `.await` points) that would be very difficult to replicate in a generated struct
196///
197/// The `Tick` type parameter is bounded by [`TickClosure`] (not `AsyncFnMut` directly) to
198/// support type erasure via [`TickClosureErased`] / [`DfirErased`] for heterogeneous
199/// collections (e.g., the sim runtime storing multiple locations in a `Vec`). The concrete
200/// (non-erased) path used by trybuild and embedded has zero overhead.
201///
202/// # Metrics
203///
204/// Whether this `Dfir` tracks runtime metrics is determined statically by
205/// [`TickClosure::Metrics`] (see [`MetricsState`]). The [`Self::metrics`] and
206/// [`Self::metrics_intervals`] accessors only exist when `Tick::Metrics = DfirMetrics`. When
207/// returning a `Dfir` from a function via `impl TickClosure`, use
208/// `Dfir<impl TickClosure<Metrics = DfirMetrics>>` if callers need to access the metrics.
209pub struct Dfir<Tick: TickClosure> {
210 /// Async closure which runs a single tick when called.
211 tick_closure: Tick,
212 /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
213 /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
214 wake_state: Arc<WakeState>,
215 /// The inline context, owned by `Dfir` and passed to the tick closure by reference.
216 context: Context<Tick::Metrics>,
217 /// See [`Self::meta_graph()`].
218 ///
219 /// Stored as the serialized JSON string rather than a deserialized [`DfirGraph`], since
220 /// `DfirGraph` contains `proc_macro2` types which are `!Send`. Deserialized on demand.
221 #[cfg(feature = "meta")]
222 meta_graph_json: Option<String>,
223 /// See [`Self::diagnostics()`].
224 #[cfg(feature = "meta")]
225 diagnostics: Option<Vec<Diagnostic<SerdeSpan>>>,
226}
227
228/// Trait for tick closures — abstracts over both concrete async closures (wrapped in
229/// [`TickClosureFn`]) and type-erased boxed versions ([`TickClosureErased`]).
230///
231/// The `&mut Context` parameter is owned by [`Dfir`] and lent to the
232/// closure each tick, avoiding shared-ownership overhead for the context.
233///
234/// The [`Self::Metrics`] associated type carries the [`MetricsState`] typestate of the context the
235/// closure expects, so that [`Dfir<Tick>`] can be parameterized by the closure alone.
236pub trait TickClosure {
237 /// Whether the context passed to this closure tracks metrics. See [`MetricsState`].
238 type Metrics: MetricsState;
239
240 /// Call the tick closure. Returns `true` if any subgraph received input data.
241 #[doc(hidden)]
242 fn call_tick<'a>(
243 &'a mut self,
244 ctx: &'a mut Context<Self::Metrics>,
245 ) -> impl Future<Output = bool> + 'a;
246}
247
248/// Wraps an `AsyncFnMut(&mut Context<M>) -> bool` closure as a [`TickClosure`].
249///
250/// A blanket `impl<M, F: AsyncFnMut(&mut Context<M>)> TickClosure for F` is not allowed
251/// since `M` would only be constrained via the `AsyncFnMut` bound (E0207), so the generated code
252/// wraps the tick closure in this newtype which carries `M` explicitly.
253#[doc(hidden)]
254pub struct TickClosureFn<M, F> {
255 func: F,
256 _metrics: PhantomData<fn() -> M>,
257}
258
259impl<M, F> TickClosureFn<M, F>
260where
261 M: MetricsState,
262 F: for<'a> AsyncFnMut(&'a mut Context<M>) -> bool,
263{
264 /// Wrap the given async tick closure.
265 pub fn new(func: F) -> Self {
266 Self {
267 func,
268 _metrics: PhantomData,
269 }
270 }
271}
272
273impl<M, F> TickClosure for TickClosureFn<M, F>
274where
275 M: MetricsState,
276 F: for<'a> AsyncFnMut(&'a mut Context<M>) -> bool,
277{
278 type Metrics = M;
279
280 fn call_tick<'a>(&'a mut self, ctx: &'a mut Context<M>) -> impl Future<Output = bool> + 'a {
281 (self.func)(ctx)
282 }
283}
284
285/// No-op `TickClosure`. Used by `dfir_syntax!` as a stand-in when codegen fails, so its
286/// `Metrics` matches the macro's default (metrics tracking enabled).
287#[doc(hidden)]
288pub struct NullTickClosure;
289
290impl TickClosure for NullTickClosure {
291 type Metrics = DfirMetrics;
292
293 fn call_tick<'a>(
294 &'a mut self,
295 _ctx: &'a mut Context<DfirMetrics>,
296 ) -> impl Future<Output = bool> + 'a {
297 std::future::ready(false)
298 }
299}
300
301/// Type-erased tick function for use in heterogeneous collections (e.g., the sim runtime).
302#[doc(hidden)]
303pub struct TickClosureErased<M: MetricsState = DfirMetrics>(Box<dyn TickClosureErasedInner<M>>);
304
305/// Object-safe inner trait for [`TickClosureErased`]. Needed because [`TickClosure::call_tick`]
306/// returns `impl Future` (not object-safe), but a trait with `&mut self -> Pin<Box<dyn Future + '_>>`
307/// is — the returned future borrows from the trait object which owns the closure.
308trait TickClosureErasedInner<M: MetricsState> {
309 fn call_tick<'a>(
310 &'a mut self,
311 ctx: &'a mut Context<M>,
312 ) -> Pin<Box<dyn Future<Output = bool> + 'a>>;
313}
314
315impl<Tick: TickClosure> TickClosureErasedInner<Tick::Metrics> for Tick {
316 fn call_tick<'a>(
317 &'a mut self,
318 ctx: &'a mut Context<Tick::Metrics>,
319 ) -> Pin<Box<dyn Future<Output = bool> + 'a>> {
320 Box::pin(TickClosure::call_tick(self, ctx))
321 }
322}
323
324impl<M: MetricsState> TickClosure for TickClosureErased<M> {
325 type Metrics = M;
326
327 fn call_tick<'a>(&'a mut self, ctx: &'a mut Context<M>) -> impl Future<Output = bool> + 'a {
328 self.0.call_tick(ctx)
329 }
330}
331
332/// Type alias for a type-erased [`Dfir`] that can be stored in heterogeneous collections.
333/// Created via [`Dfir::into_erased`].
334///
335/// `M` is the [`MetricsState`] of the erased dataflow: `DfirMetrics` (the default) if metrics
336/// are tracked, or `()` if not.
337pub type DfirErased<M = DfirMetrics> = Dfir<TickClosureErased<M>>;
338
339impl<Tick: TickClosure> Dfir<Tick> {
340 /// Create a new `Dfir` from a tick closure, inline context,
341 /// and meta graph / diagnostics JSON strings.
342 #[doc(hidden)]
343 pub fn new(
344 tick_closure: Tick,
345 context: Context<Tick::Metrics>,
346 meta_graph_json: Option<&str>,
347 diagnostics_json: Option<&str>,
348 ) -> Self {
349 #[cfg(not(feature = "meta"))]
350 let _ = (meta_graph_json, diagnostics_json);
351 Self {
352 tick_closure,
353 wake_state: context.wake_state.clone(),
354 context,
355 #[cfg(feature = "meta")]
356 meta_graph_json: meta_graph_json.map(str::to_owned),
357 #[cfg(feature = "meta")]
358 diagnostics: diagnostics_json.map(|json| {
359 serde_json::from_str(json).expect("Failed to deserialize diagnostics.")
360 }),
361 }
362 }
363
364 /// Returns the meta graph, if set.
365 ///
366 /// The graph is deserialized from its JSON representation on each call, so callers which need
367 /// it multiple times should store the result.
368 #[cfg(feature = "meta")]
369 #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
370 pub fn meta_graph(&self) -> Option<DfirGraph> {
371 self.meta_graph_json.as_deref().map(|json| {
372 let mut meta_graph: DfirGraph =
373 serde_json::from_str(json).expect("Failed to deserialize graph.");
374 let mut op_inst_diagnostics = Diagnostics::new();
375 meta_graph.insert_node_op_insts_all(&mut op_inst_diagnostics);
376 assert!(
377 op_inst_diagnostics.is_empty(),
378 "Expected no diagnostics, got: {:#?}",
379 op_inst_diagnostics
380 );
381 meta_graph
382 })
383 }
384
385 /// Returns any diagnostics generated by the surface syntax macro.
386 #[cfg(feature = "meta")]
387 #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
388 pub fn diagnostics(&self) -> Option<&[Diagnostic<SerdeSpan>]> {
389 self.diagnostics.as_deref()
390 }
391
392 /// Gets the current tick (local time) count.
393 pub fn current_tick(&self) -> TickInstant {
394 self.context.current_tick()
395 }
396}
397
398/// Metrics accessors, only available when metrics tracking is enabled (`Tick::Metrics = DfirMetrics`).
399impl<Tick: TickClosure<Metrics = DfirMetrics>> Dfir<Tick> {
400 /// Returns a reference-counted handle to the continually-updated runtime metrics for this DFIR instance.
401 ///
402 /// Only available when metrics tracking is enabled, see [`MetricsState`].
403 pub fn metrics(&self) -> Rc<DfirMetrics> {
404 Rc::clone(self.context.metrics())
405 }
406
407 /// Returns a [`DfirMetricsIntervals`] handle where each call to
408 /// [`DfirMetricsIntervals::take_interval`] ends the current interval and returns its metrics.
409 ///
410 /// The first call to `take_interval` returns metrics since this DFIR instance was created. Each subsequent call to
411 /// `take_interval` returns metrics since the previous call.
412 ///
413 /// Cloning the handle "forks" it from the original, as afterwards each interval may return different metrics
414 /// depending on when exactly `take_interval` is called.
415 ///
416 /// Only available when metrics tracking is enabled, see [`MetricsState`].
417 pub fn metrics_intervals(&self) -> DfirMetricsIntervals {
418 DfirMetricsIntervals {
419 curr: self.metrics(),
420 prev: None,
421 }
422 }
423}
424
425impl<Tick: TickClosure> Dfir<Tick> {
426 /// Spawns all tasks buffered via [`Context::request_task`] onto the current tokio runtime.
427 ///
428 /// This drains the buffer, so subsequent calls are no-ops until new tasks are requested.
429 ///
430 /// # Panics
431 ///
432 /// Panics if there are buffered tasks and this is called outside of a tokio runtime.
433 #[cfg(feature = "tokio")]
434 fn spawn_tasks(&mut self) {
435 for task in self.context.tasks_to_spawn.drain(..) {
436 tokio::spawn(task);
437 }
438 }
439
440 /// Run a single tick. Returns `true` if any subgraph received input data.
441 ///
442 /// Checks both handoff buffers (via `work_done` flag set in generated recv port code)
443 /// and external events (via `can_start_tick` set by `waker`/`schedule_subgraph`).
444 pub async fn run_tick(&mut self) -> bool {
445 #[cfg(feature = "tokio")]
446 self.spawn_tasks();
447 let had_external = self
448 .wake_state
449 .can_start_tick
450 .swap(false, Ordering::Relaxed);
451 let tick_had_work = self.tick_closure.call_tick(&mut self.context).await;
452 had_external || tick_had_work || self.wake_state.can_start_tick.load(Ordering::Relaxed)
453 }
454
455 /// Run a single tick synchronously. Panics if the tick yields (async suspension).
456 /// Returns `true` if work was done (see [`Self::run_tick`]).
457 pub fn run_tick_sync(&mut self) -> bool {
458 let mut fut = std::pin::pin!(self.run_tick());
459 let mut ctx = std::task::Context::from_waker(std::task::Waker::noop());
460 match fut.as_mut().poll(&mut ctx) {
461 std::task::Poll::Ready(result) => result,
462 std::task::Poll::Pending => {
463 panic!("Dfir::run_tick_sync: tick yielded asynchronously.")
464 }
465 }
466 }
467
468 /// Run ticks as long as work is available, then return.
469 #[cfg(feature = "tokio")]
470 pub async fn run_available(&mut self) {
471 // Always run at least one tick.
472 self.wake_state
473 .can_start_tick
474 .store(false, Ordering::Relaxed);
475 loop {
476 self.run_tick().await;
477 let can_start_tick = self
478 .wake_state
479 .can_start_tick
480 .swap(false, Ordering::Relaxed);
481 if !can_start_tick {
482 break;
483 }
484 // Yield between each tick to receive more events.
485 tokio::task::yield_now().await;
486 }
487 }
488
489 /// [`Self::run_available`] but panics if any tick yields asynchronously.
490 pub fn run_available_sync(&mut self) {
491 self.wake_state
492 .can_start_tick
493 .store(false, Ordering::Relaxed);
494 loop {
495 self.run_tick_sync();
496 let can_start_tick = self
497 .wake_state
498 .can_start_tick
499 .swap(false, Ordering::Relaxed);
500 if !can_start_tick {
501 break;
502 }
503 }
504 }
505
506 /// Run forever, processing ticks when work is available and yielding when idle.
507 #[cfg(feature = "tokio")]
508 pub async fn run(&mut self) -> crate::Never {
509 loop {
510 self.run_available().await;
511 // Wait for an external event to wake us.
512 std::future::poll_fn(|cx| {
513 // Register waker first to avoid race: if an event fires between
514 // the check and the register, the waker is already in place.
515 self.wake_state.task_waker.register(cx.waker());
516 if self.wake_state.can_start_tick.load(Ordering::Relaxed) {
517 std::task::Poll::Ready(())
518 } else {
519 std::task::Poll::Pending
520 }
521 })
522 .await;
523 }
524 }
525}
526
527impl<Tick: TickClosure + 'static> Dfir<Tick> {
528 /// Type-erase the tick closure for use in heterogeneous collections.
529 ///
530 /// Wraps the concrete tick closure in [`TickClosureErased`], which boxes the future
531 /// returned by each tick call. This adds one heap allocation per tick, but enables
532 /// storing multiple `Dfir`s with different closure types in a single `Vec`.
533 ///
534 /// The [`MetricsState`] typestate is preserved: the result is `DfirErased<Tick::Metrics>`.
535 ///
536 /// Only needed for the sim runtime path. The trybuild and embedded paths keep the
537 /// concrete type and pay no erasure cost.
538 pub fn into_erased(self) -> DfirErased<Tick::Metrics> {
539 Dfir {
540 tick_closure: TickClosureErased(Box::new(self.tick_closure)),
541 wake_state: self.wake_state,
542 context: self.context,
543 #[cfg(feature = "meta")]
544 meta_graph_json: self.meta_graph_json,
545 #[cfg(feature = "meta")]
546 diagnostics: self.diagnostics,
547 }
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use std::future::ready;
554
555 use static_assertions::assert_impl_all;
556
557 use super::{Context, Dfir, TickClosure};
558
559 struct SendTick;
560
561 impl TickClosure for SendTick {
562 type Metrics = ();
563
564 fn call_tick<'a>(
565 &'a mut self,
566 _ctx: &'a mut Context<()>,
567 ) -> impl Future<Output = bool> + 'a {
568 ready(false)
569 }
570 }
571
572 assert_impl_all!(Context<()>: Send);
573 assert_impl_all!(Dfir<SendTick>: Send);
574}