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 stage order
16//! 3. Systems read upstream resources, compute derived state, return
17//!    `bool` to propagate or skip downstream stages
18//!
19//! Systems are converted from plain functions via [`IntoSystem`], using
20//! the same HRTB double-bound pattern as [`IntoHandler`](crate::IntoHandler).
21//!
22//! # Supported signatures
23//!
24//! - `fn(params...) -> bool` — returns propagation decision for
25//!   staged scheduling
26//! - `fn(params...)` — void return, always propagates (`true`). Useful
27//!   for [`World::run_startup`] and systems that unconditionally
28//!   propagate.
29
30// Handler arity is architecturally required by the Param trait — handlers
31// take N typed parameters and the macro-generated dispatch impls expand
32// per-arity into call_inner functions with N + Input arguments. Module-level
33// allow rather than one inline attribute per arity expansion.
34#![allow(clippy::too_many_arguments)]
35
36use crate::handler::Param;
37use crate::world::{Registry, World};
38
39// =============================================================================
40// System trait
41// =============================================================================
42
43/// Object-safe dispatch trait for reconciliation systems.
44///
45/// Returns `bool` to control downstream propagation in a DAG scheduler.
46/// `true` means "my outputs changed, run downstream systems."
47/// `false` means "nothing changed, skip downstream."
48///
49/// # Difference from [`Handler`](crate::Handler)
50///
51/// | | Handler | System |
52/// |---|---------|--------|
53/// | Trigger | Per-event | Per-scheduler-pass |
54/// | Event param | Yes (`E`) | No |
55/// | Return | `()` | `bool` |
56/// | Purpose | React | Reconcile |
57pub trait System: Send {
58    /// Run this system. Returns `true` if downstream systems should run.
59    fn run(&mut self, world: &mut World) -> bool;
60
61    /// Returns the system's name for diagnostics.
62    fn name(&self) -> &'static str {
63        "<unnamed>"
64    }
65}
66
67// =============================================================================
68// SystemFn — concrete dispatch wrapper
69// =============================================================================
70
71/// Concrete system wrapper produced by [`IntoSystem`].
72///
73/// Stores the function, pre-resolved parameter state, and a diagnostic
74/// name. Users rarely name this type directly — use `Box<dyn System>`
75/// for type-erased storage, or let inference handle the concrete type.
76///
77/// The `Marker` parameter distinguishes bool-returning systems from
78/// void-returning ones, avoiding overlapping `System` impls.
79pub struct SystemFn<F, Params: Param, Marker = bool> {
80    f: F,
81    state: Params::State,
82    name: &'static str,
83    _marker: std::marker::PhantomData<Marker>,
84}
85
86// =============================================================================
87// IntoSystem — conversion trait
88// =============================================================================
89
90/// Converts a plain function into a [`System`].
91///
92/// Accepts two signatures:
93/// - `fn(params...) -> bool` — returns propagation decision
94/// - `fn(params...)` — void return, always propagates (`true`)
95///
96/// The `Marker` type parameter (defaulting to `bool`) distinguishes
97/// between the two. Existing code using `IntoSystem<Params>` continues
98/// to require `-> bool` with no changes.
99///
100/// Parameters are resolved from a [`Registry`] at conversion time.
101///
102/// # Closures vs named functions
103///
104/// Zero-parameter systems accept closures. For parameterized systems
105/// (one or more [`Param`] arguments), Rust's HRTB + GAT inference
106/// fails on closures — use named functions. Same limitation as
107/// [`IntoHandler`](crate::IntoHandler).
108///
109/// # Examples
110///
111/// Bool-returning (scheduler propagation):
112///
113/// ```
114/// use nexus_rt::{WorldBuilder, Res, ResMut, IntoSystem, System, Resource};
115///
116/// #[derive(Resource)]
117/// struct Val(u64);
118/// #[derive(Resource)]
119/// struct Flag(bool);
120///
121/// fn reconcile(val: Res<Val>, mut flag: ResMut<Flag>) -> bool {
122///     if val.0 > 10 {
123///         flag.0 = true;
124///         true
125///     } else {
126///         false
127///     }
128/// }
129///
130/// let mut builder = WorldBuilder::new();
131/// builder.register(Val(42));
132/// builder.register(Flag(false));
133/// let mut world = builder.build();
134///
135/// let mut sys = reconcile.into_system(world.registry());
136/// assert!(sys.run(&mut world));
137/// assert!(world.resource::<Flag>().0);
138/// ```
139///
140/// Void-returning (startup, unconditional propagation):
141///
142/// ```
143/// use nexus_rt::{WorldBuilder, ResMut, IntoSystem, System, Resource};
144///
145/// #[derive(Resource)]
146/// struct Val(u64);
147///
148/// fn initialize(mut val: ResMut<Val>) {
149///     val.0 = 42;
150/// }
151///
152/// let mut builder = WorldBuilder::new();
153/// builder.register(Val(0));
154/// let mut world = builder.build();
155///
156/// let mut sys = initialize.into_system(world.registry());
157/// assert!(sys.run(&mut world)); // void → always true
158/// assert_eq!(world.resource::<Val>().0, 42);
159/// ```
160///
161/// # Panics
162///
163/// Panics if any [`Param`] resource is not registered in
164/// the [`Registry`].
165pub trait IntoSystem<Params, Marker = bool> {
166    /// The concrete system type produced.
167    type System: System + 'static;
168
169    /// Convert this function into a system, resolving parameters from the registry.
170    fn into_system(self, registry: &Registry) -> Self::System;
171}
172
173// =============================================================================
174// Arity 0: fn() -> bool
175// =============================================================================
176
177impl<F: FnMut() -> bool + Send + 'static> IntoSystem<()> for F {
178    type System = SystemFn<F, ()>;
179
180    fn into_system(self, registry: &Registry) -> Self::System {
181        SystemFn {
182            f: self,
183            state: <() as Param>::init(registry),
184            name: std::any::type_name::<F>(),
185            _marker: std::marker::PhantomData,
186        }
187    }
188}
189
190impl<F: FnMut() -> bool + Send + 'static> System for SystemFn<F, ()> {
191    fn run(&mut self, _world: &mut World) -> bool {
192        (self.f)()
193    }
194
195    fn name(&self) -> &'static str {
196        self.name
197    }
198}
199
200// =============================================================================
201// Arity 0: fn() — void return (always propagates)
202// =============================================================================
203
204impl<F: FnMut() + Send + 'static> IntoSystem<(), ()> for F {
205    type System = SystemFn<F, (), ()>;
206
207    fn into_system(self, registry: &Registry) -> Self::System {
208        SystemFn {
209            f: self,
210            state: <() as Param>::init(registry),
211            name: std::any::type_name::<F>(),
212            _marker: std::marker::PhantomData,
213        }
214    }
215}
216
217impl<F: FnMut() + Send + 'static> System for SystemFn<F, (), ()> {
218    fn run(&mut self, _world: &mut World) -> bool {
219        (self.f)();
220        true
221    }
222
223    fn name(&self) -> &'static str {
224        self.name
225    }
226}
227
228// =============================================================================
229// Macro-generated impls (arities 1-8)
230// =============================================================================
231
232macro_rules! impl_into_system {
233    ($($P:ident),+) => {
234        impl<F: Send + 'static, $($P: Param + 'static),+> IntoSystem<($($P,)+)> for F
235        where
236            for<'a> &'a mut F: FnMut($($P,)+) -> bool
237                              + FnMut($($P::Item<'a>,)+) -> bool,
238        {
239            type System = SystemFn<F, ($($P,)+)>;
240
241            fn into_system(self, registry: &Registry) -> Self::System {
242                let state = <($($P,)+) as Param>::init(registry);
243                {
244                    #[allow(non_snake_case)]
245                    let ($($P,)+) = &state;
246                    registry.check_access(&[
247                        $(
248                            (<$P as Param>::resource_id($P),
249                             std::any::type_name::<$P>()),
250                        )+
251                    ]);
252                }
253                SystemFn {
254                    f: self,
255                    state,
256                    name: std::any::type_name::<F>(),
257                    _marker: std::marker::PhantomData,
258                }
259            }
260        }
261
262        impl<F: Send + 'static, $($P: Param + 'static),+> System
263            for SystemFn<F, ($($P,)+)>
264        where
265            for<'a> &'a mut F: FnMut($($P,)+) -> bool
266                              + FnMut($($P::Item<'a>,)+) -> bool,
267        {
268            #[allow(non_snake_case)]
269            fn run(&mut self, world: &mut World) -> bool {
270                fn call_inner<$($P),+>(
271                    mut f: impl FnMut($($P),+) -> bool,
272                    $($P: $P,)+
273                ) -> bool {
274                    f($($P),+)
275                }
276
277                // SAFETY: state was produced by init() on the same registry
278                // that built this world. Single-threaded sequential dispatch
279                // ensures no mutable aliasing across params.
280                #[cfg(debug_assertions)]
281                world.clear_borrows();
282                let ($($P,)+) = unsafe {
283                    <($($P,)+) as Param>::fetch(world, &mut self.state)
284                };
285                call_inner(&mut self.f, $($P),+)
286            }
287
288            fn name(&self) -> &'static str {
289                self.name
290            }
291        }
292    };
293}
294
295all_tuples!(impl_into_system);
296
297// =============================================================================
298// Macro-generated void impls (arities 1-8) — always returns true
299// =============================================================================
300
301macro_rules! impl_into_system_void {
302    ($($P:ident),+) => {
303        impl<F: Send + 'static, $($P: Param + 'static),+> IntoSystem<($($P,)+), ()> for F
304        where
305            for<'a> &'a mut F: FnMut($($P,)+)
306                              + FnMut($($P::Item<'a>,)+),
307        {
308            type System = SystemFn<F, ($($P,)+), ()>;
309
310            fn into_system(self, registry: &Registry) -> Self::System {
311                let state = <($($P,)+) as Param>::init(registry);
312                {
313                    #[allow(non_snake_case)]
314                    let ($($P,)+) = &state;
315                    registry.check_access(&[
316                        $(
317                            (<$P as Param>::resource_id($P),
318                             std::any::type_name::<$P>()),
319                        )+
320                    ]);
321                }
322                SystemFn {
323                    f: self,
324                    state,
325                    name: std::any::type_name::<F>(),
326                    _marker: std::marker::PhantomData,
327                }
328            }
329        }
330
331        impl<F: Send + 'static, $($P: Param + 'static),+> System
332            for SystemFn<F, ($($P,)+), ()>
333        where
334            for<'a> &'a mut F: FnMut($($P,)+)
335                              + FnMut($($P::Item<'a>,)+),
336        {
337            #[allow(non_snake_case)]
338            fn run(&mut self, world: &mut World) -> bool {
339                fn call_inner<$($P),+>(
340                    mut f: impl FnMut($($P),+),
341                    $($P: $P,)+
342                ) {
343                    f($($P),+)
344                }
345
346                // SAFETY: state was produced by init() on the same registry
347                // that built this world. Single-threaded sequential dispatch
348                // ensures no mutable aliasing across params.
349                #[cfg(debug_assertions)]
350                world.clear_borrows();
351                let ($($P,)+) = unsafe {
352                    <($($P,)+) as Param>::fetch(world, &mut self.state)
353                };
354                call_inner(&mut self.f, $($P),+);
355                true
356            }
357
358            fn name(&self) -> &'static str {
359                self.name
360            }
361        }
362    };
363}
364
365all_tuples!(impl_into_system_void);
366
367// =============================================================================
368// Tests
369// =============================================================================
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::{Local, Res, ResMut, WorldBuilder};
375
376    // -- Arity 0 ----------------------------------------------------------
377
378    fn always_true() -> bool {
379        true
380    }
381
382    #[test]
383    fn arity_0_system() {
384        let mut world = WorldBuilder::new().build();
385        let mut sys = always_true.into_system(world.registry());
386        assert!(sys.run(&mut world));
387    }
388
389    // -- Single param -----------------------------------------------------
390
391    fn check_threshold(val: Res<u64>) -> bool {
392        *val > 10
393    }
394
395    #[test]
396    fn single_param_system() {
397        let mut builder = WorldBuilder::new();
398        builder.register::<u64>(42);
399        let mut world = builder.build();
400
401        let mut sys = check_threshold.into_system(world.registry());
402        assert!(sys.run(&mut world));
403    }
404
405    #[test]
406    fn single_param_system_false() {
407        let mut builder = WorldBuilder::new();
408        builder.register::<u64>(5);
409        let mut world = builder.build();
410
411        let mut sys = check_threshold.into_system(world.registry());
412        assert!(!sys.run(&mut world));
413    }
414
415    // -- Two params -------------------------------------------------------
416
417    fn reconcile(val: Res<u64>, mut flag: ResMut<bool>) -> bool {
418        if *val > 10 {
419            *flag = true;
420            true
421        } else {
422            false
423        }
424    }
425
426    #[test]
427    fn two_param_system() {
428        let mut builder = WorldBuilder::new();
429        builder.register::<u64>(42);
430        builder.register::<bool>(false);
431        let mut world = builder.build();
432
433        let mut sys = reconcile.into_system(world.registry());
434        assert!(sys.run(&mut world));
435        assert!(*world.resource::<bool>());
436    }
437
438    // -- Box<dyn System> --------------------------------------------------
439
440    #[test]
441    fn box_dyn_system() {
442        let mut builder = WorldBuilder::new();
443        builder.register::<u64>(42);
444        let mut world = builder.build();
445
446        let mut boxed: Box<dyn System> = Box::new(check_threshold.into_system(world.registry()));
447        assert!(boxed.run(&mut world));
448    }
449
450    // -- Access conflict detection ----------------------------------------
451
452    #[test]
453    #[should_panic(expected = "conflicting access")]
454    fn system_access_conflict_panics() {
455        let mut builder = WorldBuilder::new();
456        builder.register::<u64>(0);
457        let world = builder.build();
458
459        fn bad(a: Res<u64>, b: ResMut<u64>) -> bool {
460            let _ = (*a, &*b);
461            true
462        }
463
464        let _sys = bad.into_system(world.registry());
465    }
466
467    // -- Local<T> in systems ----------------------------------------------
468
469    fn counting_system(mut count: Local<u64>, mut val: ResMut<u64>) -> bool {
470        *count += 1;
471        *val = *count;
472        *count < 3
473    }
474
475    #[test]
476    fn local_in_system() {
477        let mut builder = WorldBuilder::new();
478        builder.register::<u64>(0);
479        let mut world = builder.build();
480
481        let mut sys = counting_system.into_system(world.registry());
482        assert!(sys.run(&mut world)); // count=1 < 3
483        assert!(sys.run(&mut world)); // count=2 < 3
484        assert!(!sys.run(&mut world)); // count=3, not < 3
485        assert_eq!(*world.resource::<u64>(), 3);
486    }
487
488    // -- Name -------------------------------------------------------------
489
490    #[test]
491    fn system_has_name() {
492        let world = WorldBuilder::new().build();
493        let sys = always_true.into_system(world.registry());
494        assert!(sys.name().contains("always_true"));
495    }
496
497    // -- Void-returning systems -----------------------------------------------
498
499    fn noop() {}
500
501    #[test]
502    fn arity_0_void_system() {
503        let mut world = WorldBuilder::new().build();
504        let mut sys = noop.into_system(world.registry());
505        assert!(sys.run(&mut world));
506    }
507
508    fn write_val(mut v: ResMut<u64>) {
509        *v = 99;
510    }
511
512    #[test]
513    fn arity_n_void_system() {
514        let mut builder = WorldBuilder::new();
515        builder.register::<u64>(0);
516        let mut world = builder.build();
517
518        let mut sys = write_val.into_system(world.registry());
519        assert!(sys.run(&mut world));
520        assert_eq!(*world.resource::<u64>(), 99);
521    }
522
523    #[test]
524    fn box_dyn_void_system() {
525        let mut builder = WorldBuilder::new();
526        builder.register::<u64>(0);
527        let mut world = builder.build();
528
529        let mut boxed: Box<dyn System> = Box::new(write_val.into_system(world.registry()));
530        assert!(boxed.run(&mut world));
531        assert_eq!(*world.resource::<u64>(), 99);
532    }
533
534    fn void_read_only(val: Res<u64>, flag: Res<bool>) {
535        let _ = (*val, *flag);
536    }
537
538    #[test]
539    fn void_two_params_read_only() {
540        let mut builder = WorldBuilder::new();
541        builder.register::<u64>(42);
542        builder.register::<bool>(true);
543        let mut world = builder.build();
544
545        let mut sys = void_read_only.into_system(world.registry());
546        assert!(sys.run(&mut world));
547    }
548
549    fn void_two_params_write(mut a: ResMut<u64>, mut b: ResMut<bool>) {
550        *a = 77;
551        *b = true;
552    }
553
554    #[test]
555    fn void_two_params_mixed() {
556        let mut builder = WorldBuilder::new();
557        builder.register::<u64>(0);
558        builder.register::<bool>(false);
559        let mut world = builder.build();
560
561        let mut sys = void_two_params_write.into_system(world.registry());
562        assert!(sys.run(&mut world));
563        assert_eq!(*world.resource::<u64>(), 77);
564        assert!(*world.resource::<bool>());
565    }
566
567    fn void_with_local(mut count: Local<u64>, mut out: ResMut<u64>) {
568        *count += 1;
569        *out = *count;
570    }
571
572    #[test]
573    fn void_local_persists() {
574        let mut builder = WorldBuilder::new();
575        builder.register::<u64>(0);
576        let mut world = builder.build();
577
578        let mut sys = void_with_local.into_system(world.registry());
579        assert!(sys.run(&mut world));
580        assert_eq!(*world.resource::<u64>(), 1);
581        assert!(sys.run(&mut world));
582        assert_eq!(*world.resource::<u64>(), 2);
583        assert!(sys.run(&mut world));
584        assert_eq!(*world.resource::<u64>(), 3);
585    }
586
587    #[test]
588    fn void_system_has_name() {
589        let mut builder = WorldBuilder::new();
590        builder.register::<u64>(0);
591        let world = builder.build();
592
593        let sys = write_val.into_system(world.registry());
594        assert!(sys.name().contains("write_val"));
595    }
596
597    #[test]
598    #[should_panic(expected = "conflicting access")]
599    fn void_system_access_conflict_panics() {
600        let mut builder = WorldBuilder::new();
601        builder.register::<u64>(0);
602        let world = builder.build();
603
604        fn bad_void(a: Res<u64>, b: ResMut<u64>) {
605            let _ = (*a, &*b);
606        }
607
608        let _sys = bad_void.into_system(world.registry());
609    }
610}