Skip to main content

nexus_rt/
system.rs

1//! Reconciliation systems with boolean propagation.
2//!
3//! [`System`] is the dispatch trait for per-pass reconciliation logic.
4//! Unlike [`Handler`](crate::Handler) (reactive, per-event, no return
5//! value), systems return `bool` to control DAG traversal in a
6//! [`SystemScheduler`](crate::scheduler::SystemScheduler).
7//!
8//! # When to use System vs Handler
9//!
10//! Use **Handler** when reacting to external events (market data, IO,
11//! timers). Use **System** when reconciling derived state after events
12//! have been processed. The typical pattern:
13//!
14//! 1. Event handler writes to resources (`ResMut<MidPrice>`)
15//! 2. Scheduler runs systems in topological order
16//! 3. Systems read upstream resources, compute derived state, return
17//!    `bool` to propagate or skip downstream
18//!
19//! Systems are converted from plain functions via [`IntoSystem`], using
20//! the same HRTB double-bound pattern as [`IntoHandler`](crate::IntoHandler).
21//! The function signature is `fn(params...) -> bool` — no event parameter.
22
23use crate::handler::Param;
24use crate::world::{Registry, World};
25
26// =============================================================================
27// System trait
28// =============================================================================
29
30/// Object-safe dispatch trait for reconciliation systems.
31///
32/// Returns `bool` to control downstream propagation in a DAG scheduler.
33/// `true` means "my outputs changed, run downstream systems."
34/// `false` means "nothing changed, skip downstream."
35///
36/// # Difference from [`Handler`](crate::Handler)
37///
38/// | | Handler | System |
39/// |---|---------|--------|
40/// | Trigger | Per-event | Per-scheduler-pass |
41/// | Event param | Yes (`E`) | No |
42/// | Return | `()` | `bool` |
43/// | Purpose | React | Reconcile |
44pub trait System: Send {
45    /// Run this system. Returns `true` if downstream systems should run.
46    fn run(&mut self, world: &mut World) -> bool;
47
48    /// Returns the system's name for diagnostics.
49    fn name(&self) -> &'static str {
50        "<unnamed>"
51    }
52}
53
54// =============================================================================
55// SystemFn — concrete dispatch wrapper
56// =============================================================================
57
58/// Concrete system wrapper produced by [`IntoSystem`].
59///
60/// Stores the function, pre-resolved parameter state, and a diagnostic
61/// name. Users rarely name this type directly — use `Box<dyn System>`
62/// for type-erased storage, or let inference handle the concrete type.
63pub struct SystemFn<F, Params: Param> {
64    f: F,
65    state: Params::State,
66    name: &'static str,
67}
68
69// =============================================================================
70// IntoSystem — conversion trait
71// =============================================================================
72
73/// Converts a plain function into a [`System`].
74///
75/// The function signature is `fn(params...) -> bool` — no event parameter.
76/// Parameters are resolved from a [`Registry`] at conversion time.
77///
78/// # Closures vs named functions
79///
80/// Zero-parameter systems (`fn() -> bool`) accept closures. For
81/// parameterized systems (one or more [`Param`] arguments), Rust's
82/// HRTB + GAT inference fails on closures — use named functions.
83/// Same limitation as [`IntoHandler`](crate::IntoHandler).
84///
85/// # Examples
86///
87/// ```
88/// use nexus_rt::{WorldBuilder, Res, ResMut, IntoSystem, System};
89///
90/// fn reconcile(val: Res<u64>, mut flag: ResMut<bool>) -> bool {
91///     if *val > 10 {
92///         *flag = true;
93///         true
94///     } else {
95///         false
96///     }
97/// }
98///
99/// let mut builder = WorldBuilder::new();
100/// builder.register::<u64>(42);
101/// builder.register::<bool>(false);
102/// let mut world = builder.build();
103///
104/// let mut sys = reconcile.into_system(world.registry());
105/// assert!(sys.run(&mut world));
106/// assert!(*world.resource::<bool>());
107/// ```
108///
109/// # Panics
110///
111/// Panics if any [`Param`](crate::Param) resource is not registered in
112/// the [`Registry`].
113pub trait IntoSystem<Params> {
114    /// The concrete system type produced.
115    type System: System + 'static;
116
117    /// Convert this function into a system, resolving parameters from the registry.
118    fn into_system(self, registry: &Registry) -> Self::System;
119}
120
121// =============================================================================
122// Arity 0: fn() -> bool
123// =============================================================================
124
125impl<F: FnMut() -> bool + Send + 'static> IntoSystem<()> for F {
126    type System = SystemFn<F, ()>;
127
128    fn into_system(self, registry: &Registry) -> Self::System {
129        SystemFn {
130            f: self,
131            state: <() as Param>::init(registry),
132            name: std::any::type_name::<F>(),
133        }
134    }
135}
136
137impl<F: FnMut() -> bool + Send + 'static> System for SystemFn<F, ()> {
138    fn run(&mut self, _world: &mut World) -> bool {
139        (self.f)()
140    }
141
142    fn name(&self) -> &'static str {
143        self.name
144    }
145}
146
147// =============================================================================
148// Macro-generated impls (arities 1-8)
149// =============================================================================
150
151macro_rules! impl_into_system {
152    ($($P:ident),+) => {
153        impl<F: Send + 'static, $($P: Param + 'static),+> IntoSystem<($($P,)+)> for F
154        where
155            for<'a> &'a mut F: FnMut($($P,)+) -> bool
156                              + FnMut($($P::Item<'a>,)+) -> bool,
157        {
158            type System = SystemFn<F, ($($P,)+)>;
159
160            fn into_system(self, registry: &Registry) -> Self::System {
161                let state = <($($P,)+) as Param>::init(registry);
162                {
163                    #[allow(non_snake_case)]
164                    let ($($P,)+) = &state;
165                    registry.check_access(&[
166                        $(
167                            (<$P as Param>::resource_id($P),
168                             std::any::type_name::<$P>()),
169                        )+
170                    ]);
171                }
172                SystemFn {
173                    f: self,
174                    state,
175                    name: std::any::type_name::<F>(),
176                }
177            }
178        }
179
180        impl<F: Send + 'static, $($P: Param + 'static),+> System
181            for SystemFn<F, ($($P,)+)>
182        where
183            for<'a> &'a mut F: FnMut($($P,)+) -> bool
184                              + FnMut($($P::Item<'a>,)+) -> bool,
185        {
186            #[allow(non_snake_case)]
187            fn run(&mut self, world: &mut World) -> bool {
188                #[allow(clippy::too_many_arguments)]
189                fn call_inner<$($P),+>(
190                    mut f: impl FnMut($($P),+) -> bool,
191                    $($P: $P,)+
192                ) -> bool {
193                    f($($P),+)
194                }
195
196                // SAFETY: state was produced by init() on the same registry
197                // that built this world. Single-threaded sequential dispatch
198                // ensures no mutable aliasing across params.
199                #[cfg(debug_assertions)]
200                world.clear_borrows();
201                let ($($P,)+) = unsafe {
202                    <($($P,)+) as Param>::fetch(world, &mut self.state)
203                };
204                call_inner(&mut self.f, $($P),+)
205            }
206
207            fn name(&self) -> &'static str {
208                self.name
209            }
210        }
211    };
212}
213
214macro_rules! all_tuples {
215    ($m:ident) => {
216        $m!(P0);
217        $m!(P0, P1);
218        $m!(P0, P1, P2);
219        $m!(P0, P1, P2, P3);
220        $m!(P0, P1, P2, P3, P4);
221        $m!(P0, P1, P2, P3, P4, P5);
222        $m!(P0, P1, P2, P3, P4, P5, P6);
223        $m!(P0, P1, P2, P3, P4, P5, P6, P7);
224    };
225}
226
227all_tuples!(impl_into_system);
228
229// =============================================================================
230// Tests
231// =============================================================================
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::{Local, Res, ResMut, WorldBuilder};
237
238    // -- Arity 0 ----------------------------------------------------------
239
240    fn always_true() -> bool {
241        true
242    }
243
244    #[test]
245    fn arity_0_system() {
246        let mut world = WorldBuilder::new().build();
247        let mut sys = always_true.into_system(world.registry());
248        assert!(sys.run(&mut world));
249    }
250
251    // -- Single param -----------------------------------------------------
252
253    fn check_threshold(val: Res<u64>) -> bool {
254        *val > 10
255    }
256
257    #[test]
258    fn single_param_system() {
259        let mut builder = WorldBuilder::new();
260        builder.register::<u64>(42);
261        let mut world = builder.build();
262
263        let mut sys = check_threshold.into_system(world.registry());
264        assert!(sys.run(&mut world));
265    }
266
267    #[test]
268    fn single_param_system_false() {
269        let mut builder = WorldBuilder::new();
270        builder.register::<u64>(5);
271        let mut world = builder.build();
272
273        let mut sys = check_threshold.into_system(world.registry());
274        assert!(!sys.run(&mut world));
275    }
276
277    // -- Two params -------------------------------------------------------
278
279    fn reconcile(val: Res<u64>, mut flag: ResMut<bool>) -> bool {
280        if *val > 10 {
281            *flag = true;
282            true
283        } else {
284            false
285        }
286    }
287
288    #[test]
289    fn two_param_system() {
290        let mut builder = WorldBuilder::new();
291        builder.register::<u64>(42);
292        builder.register::<bool>(false);
293        let mut world = builder.build();
294
295        let mut sys = reconcile.into_system(world.registry());
296        assert!(sys.run(&mut world));
297        assert!(*world.resource::<bool>());
298    }
299
300    // -- Box<dyn System> --------------------------------------------------
301
302    #[test]
303    fn box_dyn_system() {
304        let mut builder = WorldBuilder::new();
305        builder.register::<u64>(42);
306        let mut world = builder.build();
307
308        let mut boxed: Box<dyn System> = Box::new(check_threshold.into_system(world.registry()));
309        assert!(boxed.run(&mut world));
310    }
311
312    // -- Access conflict detection ----------------------------------------
313
314    #[test]
315    #[should_panic(expected = "conflicting access")]
316    fn system_access_conflict_panics() {
317        let mut builder = WorldBuilder::new();
318        builder.register::<u64>(0);
319        let world = builder.build();
320
321        fn bad(a: Res<u64>, b: ResMut<u64>) -> bool {
322            let _ = (*a, &*b);
323            true
324        }
325
326        let _sys = bad.into_system(world.registry());
327    }
328
329    // -- Local<T> in systems ----------------------------------------------
330
331    fn counting_system(mut count: Local<u64>, mut val: ResMut<u64>) -> bool {
332        *count += 1;
333        *val = *count;
334        *count < 3
335    }
336
337    #[test]
338    fn local_in_system() {
339        let mut builder = WorldBuilder::new();
340        builder.register::<u64>(0);
341        let mut world = builder.build();
342
343        let mut sys = counting_system.into_system(world.registry());
344        assert!(sys.run(&mut world)); // count=1 < 3
345        assert!(sys.run(&mut world)); // count=2 < 3
346        assert!(!sys.run(&mut world)); // count=3, not < 3
347        assert_eq!(*world.resource::<u64>(), 3);
348    }
349
350    // -- Name -------------------------------------------------------------
351
352    #[test]
353    fn system_has_name() {
354        let world = WorldBuilder::new().build();
355        let sys = always_true.into_system(world.registry());
356        assert!(sys.name().contains("always_true"));
357    }
358}