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}