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}