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}