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