nexus_rt/world.rs
1//! Type-erased singleton resource storage.
2//!
3//! [`World`] is a unified store where each resource type gets a direct pointer
4//! ([`ResourceId`]) for O(1) dispatch-time access. Registration happens through
5//! [`WorldBuilder`], which freezes into an immutable [`World`] container via
6//! [`build()`](WorldBuilder::build).
7//!
8//! The type [`Registry`] maps types to [`ResourceId`] pointers. It is shared
9//! between [`WorldBuilder`] and [`World`], and is passed to [`Param::init`] and
10//! [`IntoHandler::into_handler`](crate::IntoHandler::into_handler) so that handlers can resolve their parameter
11//! state during driver setup — before or after `build()`.
12//!
13//! # Lifecycle
14//!
15//! ```text
16//! let mut builder = WorldBuilder::new();
17//! builder.register::<PriceCache>(value);
18//! builder.register::<TimerDriver>(value);
19//!
20//! // Drivers can resolve handlers against builder.registry()
21//! // before World is built.
22//!
23//! let world = builder.build(); // → World (frozen)
24//! ```
25//!
26//! After `build()`, the container is frozen — no inserts, no removes. All
27//! [`ResourceId`] values are valid for the lifetime of the [`World`] container.
28
29use std::any::{TypeId, type_name};
30use std::cell::Cell;
31#[cfg(debug_assertions)]
32use std::cell::UnsafeCell;
33use std::marker::PhantomData;
34use std::ptr::NonNull;
35use std::sync::Arc;
36use std::sync::atomic::AtomicBool;
37
38use rustc_hash::FxHashMap;
39
40// =============================================================================
41// Debug-mode aliasing detection
42// =============================================================================
43
44/// Tracks resource accesses within a single Param::fetch phase to detect
45/// aliasing violations at runtime in debug builds.
46///
47/// Dispatch macros call [`World::clear_borrows`] then
48/// [`World::track_borrow`] for each resource fetched. If the same resource
49/// is fetched twice within a phase, we panic with a diagnostic. This catches
50/// framework bugs where two params in the same handler resolve to the same
51/// resource — something [`Registry::check_access`] catches at construction
52/// time, but this catches dynamically for dispatch paths (like [`Opaque`](crate::Opaque)
53/// closures) that bypass static analysis.
54///
55/// Only active during the narrow `Param::fetch` window — safe API methods
56/// are not tracked.
57///
58/// Completely compiled out in release builds — zero bytes, zero branches.
59#[cfg(debug_assertions)]
60pub(crate) struct BorrowTracker {
61 /// Pointer addresses accessed in current phase.
62 /// Uses `UnsafeCell` for interior mutability because `Param::fetch` /
63 /// `World::track_borrow` operate on `&World`. Single-threaded,
64 /// non-reentrant access only.
65 accessed: UnsafeCell<Vec<NonNull<u8>>>,
66}
67
68#[cfg(debug_assertions)]
69impl BorrowTracker {
70 fn new() -> Self {
71 Self {
72 accessed: UnsafeCell::new(Vec::new()),
73 }
74 }
75
76 /// Reset all tracking state. Called before each `Param::fetch` phase.
77 fn clear(&self) {
78 // SAFETY: single-threaded, non-reentrant. No other reference to
79 // the inner Vec exists during this call.
80 let ptrs = unsafe { &mut *self.accessed.get() };
81 ptrs.clear();
82 }
83
84 /// Record an access. Panics if already accessed in this phase.
85 fn track(&self, id: ResourceId) {
86 // SAFETY: single-threaded, non-reentrant. No other reference to
87 // the inner Vec exists during this call.
88 let ptrs = unsafe { &mut *self.accessed.get() };
89 assert!(
90 !ptrs.contains(&id.0),
91 "conflicting access: resource {id} was accessed by more than one parameter \
92 in the same dispatch phase",
93 );
94 ptrs.push(id.0);
95 }
96}
97
98// =============================================================================
99// Core types
100// =============================================================================
101
102/// Direct pointer identifying a resource within a [`World`] container.
103///
104/// Points to a heap-allocated `ResourceCell<T>`. Dispatch-time access is
105/// a single deref — no index lookup, no Vec indirection.
106///
107/// Obtained from [`WorldBuilder::register`], [`WorldBuilder::ensure`],
108/// [`Registry::id`], [`World::id`], or their `try_` / `_default` variants.
109#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
110pub struct ResourceId(NonNull<u8>);
111
112impl ResourceId {
113 fn as_ptr(self) -> *mut u8 {
114 self.0.as_ptr()
115 }
116}
117
118// SAFETY: `ResourceId` is a thin, copyable handle to a `ResourceCell<T>`
119// allocated and pinned for the lifetime of its `World`:
120// - every `ResourceCell<T>` is registered with `T: Send`, so the pointee is
121// safe to send between threads,
122// - the underlying pointer is stable for the `World`'s entire lifetime, and
123// - a `ResourceId` cannot be dereferenced without going through `World`,
124// which enforces the single-threaded dispatch / aliasing invariants.
125unsafe impl Send for ResourceId {}
126
127impl std::fmt::Display for ResourceId {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "{:p}", self.0)
130 }
131}
132
133/// Monotonic event sequence number for event ordering.
134///
135/// Each event processed by a driver is assigned a unique sequence number
136/// via [`World::next_sequence`]. Handlers can read the current sequence
137/// via [`Seq`](crate::Seq) or advance it via [`SeqMut`](crate::SeqMut).
138///
139/// Uses `i64` for wire-format compatibility (FIX/SBE, Protobuf, Avro
140/// all have native signed 64-bit; unsigned is awkward or absent) and to
141/// support sentinel values ([`NULL`](Self::NULL),
142/// [`UNINITIALIZED`](Self::UNINITIALIZED)) without `Option` overhead.
143///
144/// Wrapping is harmless — at one increment per event, the positive `i64`
145/// space takes ~292 years at 1 GHz to exhaust.
146///
147/// # Sentinels
148///
149/// | Constant | Value | Meaning |
150/// |----------|-------|---------|
151/// | [`NULL`](Self::NULL) | `i64::MIN` | No sequence exists (SBE null convention) |
152/// | [`UNINITIALIZED`](Self::UNINITIALIZED) | `-1` | Not yet assigned |
153/// | [`ZERO`](Self::ZERO) | `0` | Starting point before any events |
154///
155/// # Examples
156///
157/// ```
158/// use nexus_rt::Sequence;
159///
160/// let a = Sequence::ZERO;
161/// let b = a.next();
162///
163/// assert!(b > a);
164/// assert_eq!(b.get(), 1);
165/// assert_eq!(b.elapsed_since(a), 1);
166///
167/// assert!(Sequence::NULL.is_null());
168/// assert!(!Sequence::ZERO.is_null());
169/// ```
170#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
171pub struct Sequence(pub(crate) i64);
172
173impl Sequence {
174 /// SBE-compatible null — `i64::MIN`. Indicates no sequence exists.
175 ///
176 /// Maps directly to the SBE int64 null sentinel on the wire.
177 pub const NULL: Self = Self(i64::MIN);
178
179 /// Uninitialized sentinel — `-1`. Indicates a sequence has not yet
180 /// been assigned.
181 pub const UNINITIALIZED: Self = Self(-1);
182
183 /// The zero sequence — the starting point before any events.
184 pub const ZERO: Self = Self(0);
185
186 /// Create a sequence from a raw `i64` value.
187 ///
188 /// Use for construction in tests, deserialization, or replay.
189 pub const fn new(value: i64) -> Self {
190 Self(value)
191 }
192
193 /// Returns the raw `i64` value.
194 ///
195 /// Use for logging, metrics, serialization, or passing to external
196 /// systems.
197 pub const fn get(self) -> i64 {
198 self.0
199 }
200
201 /// Returns `true` if this is the [`NULL`](Self::NULL) sentinel.
202 pub const fn is_null(self) -> bool {
203 self.0 == i64::MIN
204 }
205
206 /// Returns `true` if this is the [`UNINITIALIZED`](Self::UNINITIALIZED) sentinel.
207 pub const fn is_uninitialized(self) -> bool {
208 self.0 == -1
209 }
210
211 /// Returns the next sequence number (wrapping).
212 ///
213 /// This is a pure computation — it does not advance any world state.
214 /// Use [`World::next_sequence`] to actually advance the world's
215 /// current sequence.
216 pub const fn next(self) -> Self {
217 Self(self.0.wrapping_add(1))
218 }
219
220 /// Returns the number of events between `earlier` and `self`.
221 ///
222 /// Wrapping-aware: if `self` has wrapped past `earlier`, the result
223 /// is the wrapping distance. Returns 0 if `self == earlier`.
224 pub const fn elapsed_since(self, earlier: Self) -> i64 {
225 self.0.wrapping_sub(earlier.0)
226 }
227}
228
229impl std::fmt::Display for Sequence {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 self.0.fmt(f)
232 }
233}
234
235/// Heap-allocated wrapper for a single resource value.
236///
237/// Resources are individually heap-allocated via `Box::new(ResourceCell { value })`.
238/// The `ResourceId` is the raw pointer to this cell — a single deref to reach
239/// the value with zero framework overhead.
240pub(crate) struct ResourceCell<T> {
241 pub(crate) value: T,
242}
243
244/// Reconstruct and drop a `Box<ResourceCell<T>>` from a raw pointer.
245///
246/// # Safety
247///
248/// `ptr` must have been produced by `Box::into_raw(Box::new(ResourceCell { .. }))`
249/// where the value field is `T`. Must only be called once per pointer.
250unsafe fn drop_resource<T>(ptr: *mut u8) {
251 // SAFETY: ptr was produced by Box::into_raw(Box::new(ResourceCell { .. }))
252 // where value: T. Called exactly once in Storage::drop.
253 unsafe {
254 let _ = Box::from_raw(ptr as *mut ResourceCell<T>);
255 }
256}
257
258// =============================================================================
259// Registry — type-to-pointer mapping
260// =============================================================================
261
262/// Type-to-pointer mapping shared between [`WorldBuilder`] and [`World`].
263///
264/// Contains only the type registry — no storage backing. Passed to
265/// [`IntoHandler::into_handler`](crate::IntoHandler::into_handler) and
266/// [`Param::init`](crate::Param::init) so handlers can resolve
267/// [`ResourceId`]s during driver setup.
268///
269/// Obtained via [`WorldBuilder::registry()`] or [`World::registry()`].
270pub struct Registry {
271 indices: FxHashMap<TypeId, ResourceId>,
272}
273
274impl Registry {
275 pub(crate) fn new() -> Self {
276 Self {
277 indices: FxHashMap::default(),
278 }
279 }
280
281 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
282 ///
283 /// # Panics
284 ///
285 /// Panics if the resource type was not registered.
286 pub fn id<T: Resource>(&self) -> ResourceId {
287 *self
288 .indices
289 .get(&TypeId::of::<T>())
290 .unwrap_or_else(|| {
291 panic!(
292 "resource `{}` not registered — call WorldBuilder::register::<{}>(initial_value) during setup",
293 type_name::<T>(),
294 type_name::<T>()
295 )
296 })
297 }
298
299 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
300 /// type was not registered.
301 pub fn try_id<T: Resource>(&self) -> Option<ResourceId> {
302 self.indices.get(&TypeId::of::<T>()).copied()
303 }
304
305 /// Returns `true` if a resource of type `T` has been registered.
306 pub fn contains<T: Resource>(&self) -> bool {
307 self.indices.contains_key(&TypeId::of::<T>())
308 }
309
310 /// Returns the number of registered resources.
311 pub fn len(&self) -> usize {
312 self.indices.len()
313 }
314
315 /// Returns `true` if no resources have been registered.
316 pub fn is_empty(&self) -> bool {
317 self.indices.is_empty()
318 }
319
320 /// Validate that a set of parameter accesses don't conflict.
321 ///
322 /// Two accesses conflict when they target the same ResourceId (same
323 /// pointer). O(n²) pairwise comparison — handler arity is 1-8, so
324 /// this is trivially fast at build time.
325 ///
326 /// # Panics
327 ///
328 /// Panics if any resource is accessed by more than one parameter.
329 #[cold]
330 pub fn check_access(&self, accesses: &[(Option<ResourceId>, &str)]) {
331 for i in 0..accesses.len() {
332 let Some(id_i) = accesses[i].0 else { continue };
333 for j in (i + 1)..accesses.len() {
334 let Some(id_j) = accesses[j].0 else { continue };
335 assert!(
336 id_i != id_j,
337 "conflicting access: resource borrowed by `{}` conflicts with \
338 resource borrowed by `{}` in the same handler",
339 accesses[j].1,
340 accesses[i].1,
341 );
342 }
343 }
344 }
345}
346
347// =============================================================================
348// Storage — shared backing between builder and frozen container
349// =============================================================================
350
351/// Pointer + type-erased drop function for a single resource.
352struct DropEntry {
353 ptr: *mut u8,
354 drop_fn: unsafe fn(*mut u8),
355}
356
357/// Internal storage for type-erased resource cleanup.
358///
359/// Only walked during [`World::drop`] — no dispatch-time role. The actual
360/// resource data lives in individually heap-allocated `ResourceCell<T>`
361/// values, pointed to by [`ResourceId`].
362pub(crate) struct Storage {
363 drop_entries: Vec<DropEntry>,
364}
365
366impl Storage {
367 pub(crate) fn new() -> Self {
368 Self {
369 drop_entries: Vec::new(),
370 }
371 }
372
373 pub(crate) fn len(&self) -> usize {
374 self.drop_entries.len()
375 }
376
377 pub(crate) fn is_empty(&self) -> bool {
378 self.drop_entries.is_empty()
379 }
380}
381
382// SAFETY: All values stored in Storage were registered via `register<T: Send + 'static>`,
383// so every concrete type behind the raw pointers is Send. Storage exclusively owns
384// these heap allocations — they are not aliased or shared. Transferring ownership
385// to another thread is safe.
386#[allow(clippy::non_send_fields_in_send_ty)]
387unsafe impl Send for Storage {}
388
389impl Drop for Storage {
390 fn drop(&mut self) {
391 for entry in &self.drop_entries {
392 // SAFETY: each DropEntry was created in WorldBuilder::register().
393 // drop_fn is the monomorphized destructor for the concrete
394 // ResourceCell<T> behind ptr. Called exactly once here.
395 unsafe {
396 (entry.drop_fn)(entry.ptr);
397 }
398 }
399 }
400}
401
402// =============================================================================
403// WorldBuilder
404// =============================================================================
405
406/// Builder for registering resources before freezing into a [`World`] container.
407///
408/// Each resource type can only be registered once. Registration assigns a
409/// direct [`ResourceId`] pointer.
410///
411/// The [`registry()`](Self::registry) method exposes the type-to-pointer mapping
412/// so that drivers can resolve handlers against the builder before `build()`.
413///
414/// # Examples
415///
416/// ```
417/// use nexus_rt::{WorldBuilder, Resource};
418///
419/// #[derive(Resource)]
420/// struct Counter(u64);
421/// #[derive(Resource)]
422/// struct Flag(bool);
423///
424/// let mut builder = WorldBuilder::new();
425/// let id = builder.register(Counter(42));
426/// builder.register(Flag(true));
427/// let world = builder.build();
428///
429/// unsafe {
430/// assert_eq!(world.get::<Counter>(id).0, 42);
431/// }
432/// ```
433pub struct WorldBuilder {
434 registry: Registry,
435 storage: Storage,
436}
437
438/// Marker trait for types that can be stored in a [`World`].
439///
440/// Requires `Send + 'static`. Use `#[derive(Resource)]` to implement,
441/// or [`new_resource!`](crate::new_resource) for newtype wrappers.
442///
443/// ```
444/// use nexus_rt::Resource;
445///
446/// #[derive(Resource)]
447/// struct OrderBook {
448/// bids: Vec<(f64, f64)>,
449/// asks: Vec<(f64, f64)>,
450/// }
451/// ```
452///
453/// # Why not just `Send + 'static`?
454///
455/// Without the marker trait, two modules can independently register
456/// `u64` and silently collide. The `Resource` bound forces a newtype,
457/// making collisions a compile error.
458#[diagnostic::on_unimplemented(
459 message = "this type cannot be stored as a resource in the World",
460 note = "add `#[derive(Resource)]` to your type, or use `new_resource!` for a newtype wrapper"
461)]
462pub trait Resource: Send + 'static {}
463
464// Test-only impls for primitive types used in unit tests within this crate.
465// NOT available to external crates — they should use #[derive(Resource)] or new_resource!.
466#[cfg(test)]
467mod resource_test_impls {
468 use super::Resource;
469 impl Resource for bool {}
470 impl Resource for u32 {}
471 impl Resource for u64 {}
472 impl Resource for i64 {}
473 impl Resource for f64 {}
474 impl Resource for String {}
475 impl<T: Send + 'static> Resource for Vec<T> {}
476 impl<T: Send + Sync + 'static> Resource for std::sync::Arc<T> {}
477}
478
479impl WorldBuilder {
480 /// Create an empty builder.
481 pub fn new() -> Self {
482 Self {
483 registry: Registry::new(),
484 storage: Storage::new(),
485 }
486 }
487
488 /// Register a resource and return its [`ResourceId`].
489 ///
490 /// The value is heap-allocated inside a `ResourceCell<T>` and ownership
491 /// is transferred to the container. The pointer is stable for the
492 /// lifetime of the resulting [`World`].
493 ///
494 /// # Panics
495 ///
496 /// Panics if a resource of the same type is already registered.
497 #[cold]
498 pub fn register<T: Resource>(&mut self, value: T) -> ResourceId {
499 let type_id = TypeId::of::<T>();
500 assert!(
501 !self.registry.indices.contains_key(&type_id),
502 "resource `{}` already registered",
503 type_name::<T>(),
504 );
505
506 let cell = Box::new(ResourceCell { value });
507 let raw = Box::into_raw(cell) as *mut u8;
508 // SAFETY: Box::into_raw never returns null.
509 let ptr = unsafe { NonNull::new_unchecked(raw) };
510 let id = ResourceId(ptr);
511 self.registry.indices.insert(type_id, id);
512 self.storage.drop_entries.push(DropEntry {
513 ptr: raw,
514 drop_fn: drop_resource::<T>,
515 });
516 id
517 }
518
519 /// Register a resource using its [`Default`] value and return its
520 /// [`ResourceId`].
521 ///
522 /// Equivalent to `self.register::<T>(T::default())`.
523 #[cold]
524 pub fn register_default<T: Default + Resource>(&mut self) -> ResourceId {
525 self.register(T::default())
526 }
527
528 /// Ensure a resource is registered, returning its [`ResourceId`].
529 ///
530 /// If the type is already registered, returns the existing ID and
531 /// drops `value`. If not, registers it and returns the new ID.
532 ///
533 /// Use [`register`](Self::register) when duplicate registration is a
534 /// bug that should panic. Use `ensure` when multiple plugins or
535 /// drivers may independently need the same resource type.
536 #[cold]
537 pub fn ensure<T: Resource>(&mut self, value: T) -> ResourceId {
538 if let Some(id) = self.registry.try_id::<T>() {
539 return id;
540 }
541 self.register(value)
542 }
543
544 /// Ensure a resource is registered using its [`Default`] value,
545 /// returning its [`ResourceId`].
546 ///
547 /// If the type is already registered, returns the existing ID.
548 /// If not, registers `T::default()` and returns the new ID.
549 #[cold]
550 pub fn ensure_default<T: Default + Resource>(&mut self) -> ResourceId {
551 if let Some(id) = self.registry.try_id::<T>() {
552 return id;
553 }
554 self.register(T::default())
555 }
556
557 /// Returns a shared reference to the type registry.
558 ///
559 /// Use this for construction-time calls like
560 /// [`into_handler`](crate::IntoHandler::into_handler),
561 /// [`into_callback`](crate::IntoCallback::into_callback), and
562 /// [`into_step`](crate::pipeline::IntoStep::into_step).
563 pub fn registry(&self) -> &Registry {
564 &self.registry
565 }
566
567 /// Returns a mutable reference to the type registry.
568 ///
569 /// Rarely needed — [`registry()`](Self::registry) suffices for
570 /// construction-time calls. Exists for direct mutation of the
571 /// registry if needed.
572 #[allow(dead_code)]
573 pub(crate) fn registry_mut(&mut self) -> &mut Registry {
574 &mut self.registry
575 }
576
577 /// Returns the number of registered resources.
578 pub fn len(&self) -> usize {
579 self.storage.len()
580 }
581
582 /// Returns `true` if no resources have been registered.
583 pub fn is_empty(&self) -> bool {
584 self.storage.is_empty()
585 }
586
587 /// Returns `true` if a resource of type `T` has been registered.
588 pub fn contains<T: Resource>(&self) -> bool {
589 self.registry.contains::<T>()
590 }
591
592 /// Install a plugin. The plugin is consumed and registers its
593 /// resources into this builder.
594 pub fn install_plugin(&mut self, plugin: impl crate::plugin::Plugin) -> &mut Self {
595 plugin.build(self);
596 self
597 }
598
599 /// Install a driver. The installer is consumed, registers its resources
600 /// into this builder, and returns a concrete poller for dispatch-time
601 /// polling.
602 pub fn install_driver<D: crate::driver::Installer>(&mut self, driver: D) -> D::Poller {
603 driver.install(self)
604 }
605
606 /// Freeze the builder into an immutable [`World`] container.
607 ///
608 /// After this call, no more resources can be registered. All
609 /// [`ResourceId`] values remain valid for the lifetime of the
610 /// returned [`World`].
611 ///
612 /// When the `reactors` feature is enabled, `ReactorNotify`,
613 /// `DeferredRemovals`, and
614 /// `SourceRegistry` are automatically registered
615 /// if not already present.
616 #[allow(unused_mut)]
617 pub fn build(mut self) -> World {
618 #[cfg(feature = "reactors")]
619 let (reactor_notify_id, reactor_removals_id) = {
620 self.ensure(crate::reactor::ReactorNotify::new(16, 64));
621 self.ensure(crate::reactor::DeferredRemovals::default());
622 self.ensure(crate::reactor::SourceRegistry::new());
623 (
624 self.registry.id::<crate::reactor::ReactorNotify>(),
625 self.registry.id::<crate::reactor::DeferredRemovals>(),
626 )
627 };
628
629 World {
630 registry: self.registry,
631 storage: self.storage,
632 current_sequence: Cell::new(Sequence(0)),
633 shutdown: Arc::new(AtomicBool::new(false)),
634 _not_sync: PhantomData,
635 #[cfg(feature = "reactors")]
636 reactor_notify_id,
637 #[cfg(feature = "reactors")]
638 reactor_removals_id,
639 #[cfg(feature = "reactors")]
640 reactor_events: Some(nexus_notify::Events::with_capacity(256)),
641 #[cfg(debug_assertions)]
642 borrow_tracker: BorrowTracker::new(),
643 }
644 }
645}
646
647impl Default for WorldBuilder {
648 fn default() -> Self {
649 Self::new()
650 }
651}
652
653// =============================================================================
654// World — frozen container
655// =============================================================================
656
657/// Frozen singleton resource storage.
658///
659/// Analogous to Bevy's `World`, but restricted to singleton resources
660/// (no entities, no components, no archetypes).
661///
662/// Created by [`WorldBuilder::build()`]. Resources are accessed via
663/// [`ResourceId`] direct pointers for O(1) dispatch-time access — a single
664/// pointer deref per fetch, zero framework overhead.
665///
666/// # Safe API
667///
668/// - [`resource`](Self::resource) / [`resource_mut`](Self::resource_mut) —
669/// cold-path access via HashMap lookup.
670///
671/// # Unsafe API (framework internals)
672///
673/// The low-level `get` / `get_mut` methods are `unsafe` — used by
674/// [`Param::fetch`](crate::Param) for zero-overhead dispatch.
675/// The caller must ensure no mutable aliasing.
676pub struct World {
677 /// Type-to-pointer mapping. Same registry used during build.
678 registry: Registry,
679 /// Type-erased pointer storage. Drop handled by `Storage`.
680 storage: Storage,
681 /// Current sequence number. `Cell` so handlers can advance it
682 /// through `&World` via [`SeqMut`](crate::SeqMut).
683 current_sequence: Cell<Sequence>,
684 /// Cooperative shutdown flag. Shared with [`ShutdownHandle`](crate::ShutdownHandle)
685 /// via `Arc`. Handlers access it through the [`Shutdown`](crate::Shutdown) Param.
686 shutdown: Arc<AtomicBool>,
687 /// World must not be shared across threads — it holds interior-mutable
688 /// `Cell<Sequence>` values accessed through `&self`. `!Sync` enforced by
689 /// `PhantomData<Cell<()>>`.
690 _not_sync: PhantomData<Cell<()>>,
691 /// Pre-resolved pointer to ReactorNotify for O(1) reactor operations.
692 #[cfg(feature = "reactors")]
693 reactor_notify_id: ResourceId,
694 /// Pre-resolved pointer to DeferredRemovals (avoids HashMap lookup per frame).
695 #[cfg(feature = "reactors")]
696 reactor_removals_id: ResourceId,
697 /// Pre-allocated events buffer for dispatch (avoids allocation per frame).
698 #[cfg(feature = "reactors")]
699 reactor_events: Option<nexus_notify::Events>,
700 /// Debug-only aliasing tracker. Detects duplicate resource access within
701 /// a single dispatch phase. Compiled out entirely in release builds.
702 #[cfg(debug_assertions)]
703 borrow_tracker: BorrowTracker,
704}
705
706impl World {
707 /// Convenience constructor — returns a new [`WorldBuilder`].
708 pub fn builder() -> WorldBuilder {
709 WorldBuilder::new()
710 }
711
712 /// Returns a shared reference to the type registry.
713 ///
714 /// Use this for read-only queries (e.g. [`id`](Registry::id),
715 /// [`contains`](Registry::contains)) and construction-time calls
716 /// like [`into_handler`](crate::IntoHandler::into_handler).
717 pub fn registry(&self) -> &Registry {
718 &self.registry
719 }
720
721 /// Returns a mutable reference to the type registry.
722 ///
723 /// Rarely needed — [`registry()`](Self::registry) suffices for
724 /// construction-time calls. Exists for direct mutation of the
725 /// registry if needed.
726 #[allow(dead_code)]
727 pub(crate) fn registry_mut(&mut self) -> &mut Registry {
728 &mut self.registry
729 }
730
731 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
732 ///
733 /// # Panics
734 ///
735 /// Panics if the resource type was not registered.
736 pub fn id<T: Resource>(&self) -> ResourceId {
737 self.registry.id::<T>()
738 }
739
740 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
741 /// type was not registered.
742 pub fn try_id<T: Resource>(&self) -> Option<ResourceId> {
743 self.registry.try_id::<T>()
744 }
745
746 /// Returns the number of registered resources.
747 pub fn len(&self) -> usize {
748 self.storage.len()
749 }
750
751 /// Returns `true` if no resources are stored.
752 pub fn is_empty(&self) -> bool {
753 self.storage.is_empty()
754 }
755
756 /// Returns `true` if a resource of type `T` is stored.
757 pub fn contains<T: Resource>(&self) -> bool {
758 self.registry.contains::<T>()
759 }
760
761 // =========================================================================
762 // Safe resource access (cold path — HashMap lookup per call)
763 // =========================================================================
764
765 /// Safe shared access to a resource. Cold path — resolves via HashMap.
766 ///
767 /// Takes `&self` — multiple shared references can coexist. The borrow
768 /// checker prevents mixing with [`resource_mut`](Self::resource_mut)
769 /// (which takes `&mut self`).
770 ///
771 /// # Panics
772 ///
773 /// Panics if the resource type was not registered.
774 pub fn resource<T: Resource>(&self) -> &T {
775 let id = self.registry.id::<T>();
776 // SAFETY: id resolved from our own registry. &self prevents mutable
777 // aliases — resource_mut takes &mut self.
778 unsafe { self.get(id) }
779 }
780
781 /// Safe exclusive access to a resource. Cold path — resolves via HashMap.
782 ///
783 /// # Panics
784 ///
785 /// Panics if the resource type was not registered.
786 pub fn resource_mut<T: Resource>(&mut self) -> &mut T {
787 let id = self.registry.id::<T>();
788 // SAFETY: id resolved from our own registry. &mut self ensures
789 // exclusive access — no other references can exist.
790 unsafe { self.get_mut(id) }
791 }
792
793 // =========================================================================
794 // One-shot dispatch
795 // =========================================================================
796
797 /// Run a system once with full Param resolution.
798 ///
799 /// Intended for one-shot initialization after [`build()`](WorldBuilder::build).
800 /// Accepts both void-returning (`fn(params...)`) and bool-returning
801 /// (`fn(params...) -> bool`) functions via [`IntoSystem`](crate::IntoSystem).
802 /// The return value is always ignored — startup has no DAG to
803 /// propagate through. Named functions only (same closure limitation
804 /// as [`IntoHandler`](crate::IntoHandler)).
805 ///
806 /// Can be called multiple times for phased initialization.
807 ///
808 /// # Examples
809 ///
810 /// ```ignore
811 /// fn startup(
812 /// mut driver: ResMut<MioDriver>,
813 /// mut listener: ResMut<Listener>,
814 /// ) {
815 /// // wire drivers to IO sources...
816 /// }
817 ///
818 /// let mut world = wb.build();
819 /// world.run_startup(startup);
820 /// ```
821 pub fn run_startup<F, Params, M>(&mut self, f: F)
822 where
823 F: crate::IntoSystem<Params, M>,
824 {
825 use crate::System;
826 let mut sys = f.into_system(&self.registry);
827 sys.run(self);
828 }
829
830 // =========================================================================
831 // Shutdown
832 // =========================================================================
833
834 /// Returns a [`ShutdownHandle`](crate::shutdown::ShutdownHandle)
835 /// sharing the same flag as the world's shutdown state.
836 ///
837 /// The handle is owned by the event loop and checked each iteration.
838 /// Handlers trigger shutdown via the [`Shutdown`](crate::Shutdown) Param.
839 pub fn shutdown_handle(&self) -> crate::shutdown::ShutdownHandle {
840 crate::shutdown::ShutdownHandle::new(Arc::clone(&self.shutdown))
841 }
842
843 /// Returns a reference to the shutdown flag.
844 ///
845 /// Used by the [`Shutdown`](crate::Shutdown) Param for direct access.
846 pub(crate) fn shutdown_flag(&self) -> &AtomicBool {
847 &self.shutdown
848 }
849
850 /// Run the event loop until shutdown is triggered.
851 ///
852 /// The closure receives `&mut World` and defines one iteration of
853 /// the poll loop — which drivers to poll, in what order, with what
854 /// timeout. The loop exits when a handler calls
855 /// [`Shutdown::trigger`](crate::Shutdown::trigger) or
856 /// an external signal flips the flag (see
857 /// `ShutdownHandle::enable_signals` (requires `signals` feature)).
858 ///
859 /// # Examples
860 ///
861 /// ```ignore
862 /// let mut world = wb.build();
863 /// world.run_startup(startup);
864 ///
865 /// world.run(|world| {
866 /// let now = Instant::now();
867 /// let timeout = timer.next_deadline(world)
868 /// .map(|d| d.saturating_duration_since(now));
869 /// mio.poll(world, timeout).expect("mio poll");
870 /// timer.poll(world, now);
871 /// });
872 /// ```
873 pub fn run(&mut self, mut f: impl FnMut(&mut World)) {
874 while !self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
875 f(self);
876 }
877 }
878
879 // =========================================================================
880 // Sequence
881 // =========================================================================
882
883 /// Returns the current event sequence number.
884 pub fn current_sequence(&self) -> Sequence {
885 self.current_sequence.get()
886 }
887
888 /// Advance to the next event sequence number and return it.
889 ///
890 /// Drivers call this before dispatching each event. The returned
891 /// sequence number identifies the event being processed.
892 pub fn next_sequence(&mut self) -> Sequence {
893 let next = Sequence(self.current_sequence.get().0.wrapping_add(1));
894 self.current_sequence.set(next);
895 next
896 }
897
898 /// Returns a reference to the sequence `Cell`.
899 ///
900 /// Used by [`SeqMut`](crate::SeqMut) Param for direct access.
901 pub(crate) fn sequence_cell(&self) -> &Cell<Sequence> {
902 &self.current_sequence
903 }
904
905 /// Set the current sequence number directly.
906 ///
907 /// Use for recovery / replay — restores the sequence to a known
908 /// checkpoint so that subsequent `next_sequence` calls continue
909 /// from the right point.
910 pub fn set_sequence(&mut self, seq: Sequence) {
911 self.current_sequence.set(seq);
912 }
913
914 // =========================================================================
915 // Unsafe resource access (hot path — pre-resolved ResourceId)
916 // =========================================================================
917
918 /// Fetch a shared reference to a resource by direct pointer.
919 ///
920 /// # Safety
921 ///
922 /// - `id` must have been returned by [`WorldBuilder::register`] for
923 /// the same builder that produced this container.
924 /// - `T` must be the same type that was registered at this `id`.
925 /// - The caller must ensure no mutable reference to this resource exists.
926 #[inline(always)]
927 pub unsafe fn get<T: 'static>(&self, id: ResourceId) -> &T {
928 // SAFETY: caller guarantees id was returned by register() on the
929 // builder that produced this container. T matches the registered type.
930 // No mutable alias exists. ResourceId points to a valid ResourceCell<T>.
931 unsafe { &(*(id.as_ptr() as *const ResourceCell<T>)).value }
932 }
933
934 /// Fetch a mutable reference to a resource by direct pointer.
935 ///
936 /// Takes `&self` — the container structure is frozen, but individual
937 /// resources have interior mutability via raw pointers. Sound because
938 /// callers (single-threaded sequential dispatch) uphold no-aliasing.
939 ///
940 /// # Safety
941 ///
942 /// - `id` must have been returned by [`WorldBuilder::register`] for
943 /// the same builder that produced this container.
944 /// - `T` must be the same type that was registered at this `id`.
945 /// - The caller must ensure no other reference (shared or mutable) to this
946 /// resource exists.
947 #[inline(always)]
948 #[allow(clippy::mut_from_ref)]
949 pub unsafe fn get_mut<T: 'static>(&self, id: ResourceId) -> &mut T {
950 // SAFETY: caller guarantees id was returned by register() on the
951 // builder that produced this container. T matches the registered type.
952 // No aliases exist. ResourceId points to a valid ResourceCell<T>.
953 unsafe { &mut (*(id.as_ptr() as *mut ResourceCell<T>)).value }
954 }
955
956 /// Reset borrow tracking for a new dispatch phase.
957 ///
958 /// Called before each [`Param::fetch`](crate::Param::fetch) in dispatch
959 /// macros. Only exists in debug builds.
960 #[cfg(debug_assertions)]
961 pub(crate) fn clear_borrows(&self) {
962 self.borrow_tracker.clear();
963 }
964
965 /// Record a resource access in the debug borrow tracker.
966 ///
967 /// Called by [`Param::fetch`](crate::Param::fetch) impls for each
968 /// resource parameter. Panics if the resource was already accessed
969 /// in the current phase (since the last [`clear_borrows`](Self::clear_borrows)).
970 /// Only exists in debug builds.
971 #[cfg(debug_assertions)]
972 pub(crate) fn track_borrow(&self, id: ResourceId) {
973 self.borrow_tracker.track(id);
974 }
975
976 // =========================================================================
977 // Reactor convenience methods (behind `reactors` feature)
978 // =========================================================================
979
980 /// Register a new data source for reactor subscriptions.
981 ///
982 /// Convenience for `world.resource_mut::<ReactorNotify>().register_source()`.
983 #[cfg(feature = "reactors")]
984 pub fn register_source(&mut self) -> crate::reactor::DataSource {
985 self.resource_mut::<crate::reactor::ReactorNotify>()
986 .register_source()
987 }
988
989 /// Create and register a reactor from a step function + context factory.
990 ///
991 /// The closure receives the assigned [`Token`](nexus_notify::Token) so
992 /// the reactor can store it for wire routing or self-deregistration.
993 /// Params are resolved from the internal registry — single pointer
994 /// deref at dispatch time.
995 ///
996 /// This is the primary registration API. It handles the borrow
997 /// juggling internally: allocates the token, resolves params from
998 /// the registry, and inserts the reactor — all in one call.
999 ///
1000 /// # Example
1001 ///
1002 /// ```ignore
1003 /// let src = world.register_source();
1004 /// world.spawn_reactor(
1005 /// |id| QuotingCtx { reactor_id: id, instrument: BTC, layer: 1 },
1006 /// quoting_step,
1007 /// ).subscribe(src);
1008 /// ```
1009 #[cfg(feature = "reactors")]
1010 pub fn spawn_reactor<C, Params, F: crate::reactor::IntoReactor<C, Params>>(
1011 &mut self,
1012 ctx_fn: impl FnOnce(nexus_notify::Token) -> C,
1013 step: F,
1014 ) -> crate::reactor::ReactorRegistration<'_> {
1015 // SAFETY: reactor_notify_id was resolved during build() from the same
1016 // WorldBuilder. ReactorNotify is heap-allocated via ResourceCell —
1017 // the pointer is stable for World's lifetime. We access it via raw
1018 // pointer to simultaneously read the registry (disjoint: registry is
1019 // an inline field, ReactorNotify is on the heap). &mut self guarantees
1020 // no other references to any World resource exist.
1021 let notify_ptr: *mut crate::reactor::ReactorNotify =
1022 unsafe { self.get_mut::<crate::reactor::ReactorNotify>(self.reactor_notify_id) };
1023 // SAFETY: notify_ptr is stable (heap-allocated ResourceCell).
1024 // No other references exist — previous &mut was not retained.
1025 let token = unsafe { &mut *notify_ptr }.create_reactor();
1026 let ctx = ctx_fn(token);
1027 let reactor = step.into_reactor(ctx, &self.registry);
1028 // SAFETY: notify_ptr is stable (heap-allocated ResourceCell).
1029 // No other references exist — ctx_fn and into_reactor don't alias.
1030 let notify = unsafe { &mut *notify_ptr };
1031 notify.insert_reactor(token, reactor)
1032 }
1033
1034 /// Register a pre-built reactor in one step.
1035 ///
1036 /// For reactors that don't need their token in the context, or for
1037 /// [`PipelineReactor`](crate::PipelineReactor) instances.
1038 #[cfg(feature = "reactors")]
1039 pub fn spawn_built_reactor(
1040 &mut self,
1041 reactor: impl crate::reactor::Reactor + 'static,
1042 ) -> crate::reactor::ReactorRegistration<'_> {
1043 // SAFETY: reactor_notify_id was resolved during build() from the
1044 // same WorldBuilder. &mut self guarantees exclusive access.
1045 let notify =
1046 unsafe { self.get_mut::<crate::reactor::ReactorNotify>(self.reactor_notify_id) };
1047 notify.register_built(reactor)
1048 }
1049
1050 /// Dispatch all woken reactors and process deferred removals.
1051 ///
1052 /// Call this post-frame after event handlers have called
1053 /// [`ReactorNotify::mark`](crate::ReactorNotify::mark).
1054 /// Returns `true` if any reactor ran.
1055 #[cfg(feature = "reactors")]
1056 pub fn dispatch_reactors(&mut self) -> bool {
1057 // SAFETY: reactor_notify_id was resolved during build() from the
1058 // same WorldBuilder. &mut self guarantees exclusive access. We hold
1059 // a raw pointer to allow scoped re-borrows across reactor dispatch.
1060 let notify_ptr: *mut crate::reactor::ReactorNotify =
1061 unsafe { self.get_mut::<crate::reactor::ReactorNotify>(self.reactor_notify_id) };
1062
1063 // Poll — scoped &mut, dropped before reactor dispatch.
1064 let mut events = self
1065 .reactor_events
1066 .take()
1067 .unwrap_or_else(|| nexus_notify::Events::with_capacity(256));
1068 {
1069 // SAFETY: notify_ptr is stable (heap-allocated ResourceCell).
1070 // Scoped — &mut is dropped before any other access.
1071 let notify = unsafe { &mut *notify_ptr };
1072 notify.poll(&mut events);
1073 }
1074 let ran = !events.is_empty();
1075
1076 // Dispatch — each reactor is moved out before run(), put back after.
1077 // &mut ReactorNotify is scoped tightly to avoid aliasing during run().
1078 for token in events.iter() {
1079 let idx = token.index();
1080 let reactor = {
1081 // SAFETY: notify_ptr is stable. Scoped — &mut is dropped
1082 // before reactor.run(self) which may re-borrow World.
1083 let notify = unsafe { &mut *notify_ptr };
1084 notify.take_reactor(idx)
1085 }; // &mut dropped here — safe to call run()
1086 if let Some(mut reactor) = reactor {
1087 reactor.run(self);
1088 // SAFETY: notify_ptr is stable. reactor.run() is complete,
1089 // so no World borrows remain. Safe to re-borrow notify.
1090 let notify = unsafe { &mut *notify_ptr };
1091 notify.put_reactor(idx, reactor);
1092 }
1093 }
1094
1095 // Deferred removals — swap the inner Vec out to avoid holding
1096 // &mut DeferredRemovals and &mut ReactorNotify simultaneously.
1097 // Zero allocation: the Vec is swapped back and reused next frame.
1098 // SAFETY: reactor_removals_id was resolved during build() from
1099 // the same WorldBuilder. No other references to this resource.
1100 let removals =
1101 unsafe { self.get_mut::<crate::reactor::DeferredRemovals>(self.reactor_removals_id) };
1102 let mut pending = removals.take();
1103 if !pending.is_empty() {
1104 // SAFETY: notify_ptr is stable. removals &mut was dropped
1105 // (pending now owns the data). No aliasing.
1106 let notify = unsafe { &mut *notify_ptr };
1107 while let Some(token) = pending.pop() {
1108 notify.remove_reactor(token);
1109 }
1110 }
1111 // Put the (now empty) Vec back for reuse.
1112 // SAFETY: same removals_id, no other references.
1113 let removals =
1114 unsafe { self.get_mut::<crate::reactor::DeferredRemovals>(self.reactor_removals_id) };
1115 removals.put(pending);
1116
1117 // Return events buffer for reuse next frame.
1118 self.reactor_events = Some(events);
1119
1120 ran
1121 }
1122
1123 /// Number of registered reactors.
1124 #[cfg(feature = "reactors")]
1125 pub fn reactor_count(&self) -> usize {
1126 self.resource::<crate::reactor::ReactorNotify>()
1127 .reactor_count()
1128 }
1129}
1130
1131// SAFETY: All resources are `T: Send` (enforced by `register`). World owns all
1132// heap-allocated data exclusively — the raw pointers are not aliased or shared.
1133// Transferring ownership to another thread is safe; the new thread becomes the
1134// sole accessor.
1135#[allow(clippy::non_send_fields_in_send_ty)]
1136unsafe impl Send for World {}
1137
1138// =============================================================================
1139// Tests
1140// =============================================================================
1141
1142#[cfg(test)]
1143mod tests {
1144 use super::*;
1145 use std::sync::{Arc, Weak};
1146
1147 struct Price {
1148 value: f64,
1149 }
1150 impl Resource for Price {}
1151
1152 struct Venue {
1153 name: &'static str,
1154 }
1155 impl Resource for Venue {}
1156
1157 struct Config {
1158 max_orders: usize,
1159 }
1160 impl Resource for Config {}
1161
1162 #[test]
1163 fn register_and_build() {
1164 let mut builder = WorldBuilder::new();
1165 builder.register::<Price>(Price { value: 100.0 });
1166 builder.register::<Venue>(Venue { name: "test" });
1167 let world = builder.build();
1168 #[cfg(not(feature = "reactors"))]
1169 assert_eq!(world.len(), 2);
1170 #[cfg(feature = "reactors")]
1171 assert_eq!(world.len(), 5); // + ReactorNotify + DeferredRemovals + SourceRegistry
1172 }
1173
1174 #[test]
1175 #[allow(clippy::float_cmp)]
1176 fn resource_ids_are_distinct() {
1177 let mut builder = WorldBuilder::new();
1178 let id0 = builder.register::<Price>(Price { value: 0.0 });
1179 let id1 = builder.register::<Venue>(Venue { name: "" });
1180 let id2 = builder.register::<Config>(Config { max_orders: 0 });
1181 assert_ne!(id0, id1);
1182 assert_ne!(id1, id2);
1183 assert_ne!(id0, id2);
1184
1185 let world = builder.build();
1186 unsafe {
1187 assert_eq!(world.get::<Price>(id0).value, 0.0);
1188 assert_eq!(world.get::<Venue>(id1).name, "");
1189 assert_eq!(world.get::<Config>(id2).max_orders, 0);
1190 }
1191 }
1192
1193 #[test]
1194 #[allow(clippy::float_cmp)]
1195 fn get_returns_registered_value() {
1196 let mut builder = WorldBuilder::new();
1197 builder.register::<Price>(Price { value: 42.5 });
1198 let world = builder.build();
1199
1200 let id = world.id::<Price>();
1201 // SAFETY: id resolved from this container, type matches, no aliasing.
1202 let price = unsafe { world.get::<Price>(id) };
1203 assert_eq!(price.value, 42.5);
1204 }
1205
1206 #[test]
1207 #[allow(clippy::float_cmp)]
1208 fn get_mut_modifies_value() {
1209 let mut builder = WorldBuilder::new();
1210 builder.register::<Price>(Price { value: 1.0 });
1211 let world = builder.build();
1212
1213 let id = world.id::<Price>();
1214 // SAFETY: id resolved from this container, type matches, no aliasing.
1215 unsafe {
1216 world.get_mut::<Price>(id).value = 99.0;
1217 assert_eq!(world.get::<Price>(id).value, 99.0);
1218 }
1219 }
1220
1221 #[test]
1222 fn try_id_returns_none_for_unregistered() {
1223 let world = WorldBuilder::new().build();
1224 assert!(world.try_id::<Price>().is_none());
1225 }
1226
1227 #[test]
1228 fn try_id_returns_some_for_registered() {
1229 let mut builder = WorldBuilder::new();
1230 builder.register::<Price>(Price { value: 0.0 });
1231 let world = builder.build();
1232
1233 assert!(world.try_id::<Price>().is_some());
1234 }
1235
1236 #[test]
1237 #[should_panic(expected = "already registered")]
1238 fn panics_on_duplicate_registration() {
1239 let mut builder = WorldBuilder::new();
1240 builder.register::<Price>(Price { value: 1.0 });
1241 builder.register::<Price>(Price { value: 2.0 });
1242 }
1243
1244 #[test]
1245 #[should_panic(expected = "not registered")]
1246 fn panics_on_unregistered_id() {
1247 let world = WorldBuilder::new().build();
1248 world.id::<Price>();
1249 }
1250
1251 #[test]
1252 fn empty_builder_builds_empty_world() {
1253 let world = WorldBuilder::new().build();
1254 // With the `reactors` feature, ReactorNotify + DeferredRemovals +
1255 // SourceRegistry are auto-registered. Without it, the world is truly empty.
1256 #[cfg(not(feature = "reactors"))]
1257 assert_eq!(world.len(), 0);
1258 #[cfg(feature = "reactors")]
1259 assert_eq!(world.len(), 3);
1260 }
1261
1262 #[test]
1263 fn drop_runs_destructors() {
1264 let arc = Arc::new(42u32);
1265 let weak: Weak<u32> = Arc::downgrade(&arc);
1266
1267 {
1268 let mut builder = WorldBuilder::new();
1269 builder.register::<Arc<u32>>(arc);
1270 let _world = builder.build();
1271 // Arc still alive — held by World
1272 assert!(weak.upgrade().is_some());
1273 }
1274 // World dropped — Arc should be deallocated
1275 assert!(weak.upgrade().is_none());
1276 }
1277
1278 #[test]
1279 fn builder_drop_cleans_up_without_build() {
1280 let arc = Arc::new(99u32);
1281 let weak: Weak<u32> = Arc::downgrade(&arc);
1282
1283 {
1284 let mut builder = WorldBuilder::new();
1285 builder.register::<Arc<u32>>(arc);
1286 }
1287 // Builder dropped without build() — Storage::drop cleans up
1288 assert!(weak.upgrade().is_none());
1289 }
1290
1291 #[test]
1292 #[allow(clippy::float_cmp)]
1293 fn multiple_types_independent() {
1294 let mut builder = WorldBuilder::new();
1295 let price_id = builder.register::<Price>(Price { value: 10.0 });
1296 let venue_id = builder.register::<Venue>(Venue { name: "CB" });
1297 let config_id = builder.register::<Config>(Config { max_orders: 500 });
1298 let world = builder.build();
1299
1300 unsafe {
1301 assert_eq!(world.get::<Price>(price_id).value, 10.0);
1302 assert_eq!(world.get::<Venue>(venue_id).name, "CB");
1303 assert_eq!(world.get::<Config>(config_id).max_orders, 500);
1304 }
1305 }
1306
1307 #[test]
1308 fn contains_reflects_registration() {
1309 let mut builder = WorldBuilder::new();
1310 assert!(!builder.contains::<Price>());
1311
1312 builder.register::<Price>(Price { value: 0.0 });
1313 assert!(builder.contains::<Price>());
1314 assert!(!builder.contains::<Venue>());
1315
1316 let world = builder.build();
1317 assert!(world.contains::<Price>());
1318 assert!(!world.contains::<Venue>());
1319 }
1320
1321 #[test]
1322 #[allow(clippy::float_cmp)]
1323 fn send_to_another_thread() {
1324 let mut builder = WorldBuilder::new();
1325 builder.register::<Price>(Price { value: 55.5 });
1326 let world = builder.build();
1327
1328 let handle = std::thread::spawn(move || {
1329 let id = world.id::<Price>();
1330 // SAFETY: sole owner on this thread, no aliasing.
1331 unsafe { world.get::<Price>(id).value }
1332 });
1333 assert_eq!(handle.join().unwrap(), 55.5);
1334 }
1335
1336 #[test]
1337 fn registry_accessible_from_builder() {
1338 let mut builder = WorldBuilder::new();
1339 let registered_id = builder.register::<u64>(42);
1340
1341 let registry = builder.registry();
1342 assert!(registry.contains::<u64>());
1343 assert!(!registry.contains::<bool>());
1344
1345 let id = registry.id::<u64>();
1346 assert_eq!(id, registered_id);
1347 }
1348
1349 #[test]
1350 fn registry_accessible_from_world() {
1351 let mut builder = WorldBuilder::new();
1352 builder.register::<u64>(42);
1353 let world = builder.build();
1354
1355 let registry = world.registry();
1356 assert!(registry.contains::<u64>());
1357
1358 // Registry from world and world.id() agree.
1359 assert_eq!(registry.id::<u64>(), world.id::<u64>());
1360 }
1361
1362 // -- Safe accessor tests --------------------------------------------------
1363
1364 #[test]
1365 #[allow(clippy::float_cmp)]
1366 fn resource_reads_value() {
1367 let mut builder = WorldBuilder::new();
1368 builder.register::<Price>(Price { value: 42.5 });
1369 let world = builder.build();
1370
1371 assert_eq!(world.resource::<Price>().value, 42.5);
1372 }
1373
1374 #[test]
1375 fn resource_mut_modifies_value() {
1376 let mut builder = WorldBuilder::new();
1377 builder.register::<u64>(0);
1378 let mut world = builder.build();
1379
1380 *world.resource_mut::<u64>() = 99;
1381 assert_eq!(*world.resource::<u64>(), 99);
1382 }
1383
1384 #[test]
1385 fn register_default_works() {
1386 let mut builder = WorldBuilder::new();
1387 let id = builder.register_default::<Vec<u32>>();
1388 let world = builder.build();
1389
1390 assert_eq!(id, world.id::<Vec<u32>>());
1391 let v = world.resource::<Vec<u32>>();
1392 assert!(v.is_empty());
1393 }
1394
1395 #[test]
1396 fn ensure_registers_new_type() {
1397 let mut builder = WorldBuilder::new();
1398 let id = builder.ensure::<u64>(42);
1399 let world = builder.build();
1400
1401 assert_eq!(id, world.id::<u64>());
1402 assert_eq!(*world.resource::<u64>(), 42);
1403 }
1404
1405 #[test]
1406 fn ensure_returns_existing_id() {
1407 let mut builder = WorldBuilder::new();
1408 let id1 = builder.register::<u64>(42);
1409 let id2 = builder.ensure::<u64>(99);
1410 assert_eq!(id1, id2);
1411
1412 // Original value preserved, new value dropped.
1413 let world = builder.build();
1414 assert_eq!(*world.resource::<u64>(), 42);
1415 }
1416
1417 #[test]
1418 fn ensure_default_registers_new_type() {
1419 let mut builder = WorldBuilder::new();
1420 let id = builder.ensure_default::<Vec<u32>>();
1421 let world = builder.build();
1422
1423 assert_eq!(id, world.id::<Vec<u32>>());
1424 assert!(world.resource::<Vec<u32>>().is_empty());
1425 }
1426
1427 #[test]
1428 fn ensure_default_returns_existing_id() {
1429 let mut builder = WorldBuilder::new();
1430 builder.register::<Vec<u32>>(vec![1, 2, 3]);
1431 let id = builder.ensure_default::<Vec<u32>>();
1432 let world = builder.build();
1433
1434 assert_eq!(id, world.id::<Vec<u32>>());
1435 // Original value preserved.
1436 assert_eq!(*world.resource::<Vec<u32>>(), vec![1, 2, 3]);
1437 }
1438
1439 // -- Sequence tests -----------------------------------------------------------
1440
1441 #[test]
1442 fn sequence_default_is_zero() {
1443 assert_eq!(Sequence::default(), Sequence(0));
1444 }
1445
1446 #[test]
1447 fn next_sequence_increments() {
1448 let mut world = WorldBuilder::new().build();
1449 assert_eq!(world.current_sequence(), Sequence(0));
1450 world.next_sequence();
1451 assert_eq!(world.current_sequence(), Sequence(1));
1452 world.next_sequence();
1453 assert_eq!(world.current_sequence(), Sequence(2));
1454 }
1455
1456 // -- run_startup tests ----------------------------------------------------
1457
1458 #[test]
1459 fn run_startup_dispatches_handler() {
1460 use crate::ResMut;
1461
1462 let mut builder = WorldBuilder::new();
1463 builder.register::<u64>(0);
1464 builder.register::<bool>(false);
1465 let mut world = builder.build();
1466
1467 fn init(mut counter: ResMut<u64>, mut flag: ResMut<bool>) {
1468 *counter = 42;
1469 *flag = true;
1470 }
1471
1472 world.run_startup(init);
1473
1474 assert_eq!(*world.resource::<u64>(), 42);
1475 assert!(*world.resource::<bool>());
1476 }
1477
1478 #[test]
1479 fn run_startup_multiple_phases() {
1480 use crate::ResMut;
1481
1482 let mut builder = WorldBuilder::new();
1483 builder.register::<u64>(0);
1484 let mut world = builder.build();
1485
1486 fn phase1(mut counter: ResMut<u64>) {
1487 *counter += 10;
1488 }
1489
1490 fn phase2(mut counter: ResMut<u64>) {
1491 *counter += 5;
1492 }
1493
1494 world.run_startup(phase1);
1495 world.run_startup(phase2);
1496
1497 assert_eq!(*world.resource::<u64>(), 15);
1498 }
1499
1500 // -- Plugin / Driver tests ------------------------------------------------
1501
1502 #[test]
1503 fn plugin_registers_resources() {
1504 struct TestPlugin;
1505
1506 impl crate::plugin::Plugin for TestPlugin {
1507 fn build(self, world: &mut WorldBuilder) {
1508 world.register::<u64>(42);
1509 world.register::<bool>(true);
1510 }
1511 }
1512
1513 let mut builder = WorldBuilder::new();
1514 builder.install_plugin(TestPlugin);
1515 let world = builder.build();
1516
1517 assert_eq!(*world.resource::<u64>(), 42);
1518 assert!(*world.resource::<bool>());
1519 }
1520
1521 #[test]
1522 fn driver_installs_and_returns_handle() {
1523 struct TestInstaller;
1524 struct TestHandle {
1525 counter_id: ResourceId,
1526 }
1527
1528 impl crate::driver::Installer for TestInstaller {
1529 type Poller = TestHandle;
1530
1531 fn install(self, world: &mut WorldBuilder) -> TestHandle {
1532 let counter_id = world.register::<u64>(0);
1533 TestHandle { counter_id }
1534 }
1535 }
1536
1537 let mut builder = WorldBuilder::new();
1538 let handle = builder.install_driver(TestInstaller);
1539 let world = builder.build();
1540
1541 // Handle's pre-resolved ID can access the resource.
1542 unsafe {
1543 assert_eq!(*world.get::<u64>(handle.counter_id), 0);
1544 }
1545 }
1546
1547 // -- check_access conflict detection ----------------------------------------
1548
1549 #[test]
1550 fn check_access_no_conflict() {
1551 let mut builder = WorldBuilder::new();
1552 let id_a = builder.register::<u64>(0);
1553 let id_b = builder.register::<u32>(0);
1554 builder
1555 .registry()
1556 .check_access(&[(Some(id_a), "a"), (Some(id_b), "b")]);
1557 }
1558
1559 #[test]
1560 #[should_panic(expected = "conflicting access")]
1561 fn check_access_detects_conflict() {
1562 let mut builder = WorldBuilder::new();
1563 let id = builder.register::<u64>(0);
1564 builder
1565 .registry()
1566 .check_access(&[(Some(id), "a"), (Some(id), "b")]);
1567 }
1568
1569 #[test]
1570 fn sequence_wrapping() {
1571 let builder = WorldBuilder::new();
1572 let mut world = builder.build();
1573
1574 // Advance to MAX.
1575 world.current_sequence.set(Sequence(i64::MAX));
1576 assert_eq!(world.current_sequence(), Sequence(i64::MAX));
1577
1578 // Wrap to MIN (which is the NULL sentinel, but wrapping is
1579 // purely mechanical — it doesn't assign semantic meaning).
1580 let seq = world.next_sequence();
1581 assert_eq!(seq, Sequence(i64::MIN));
1582 assert_eq!(world.current_sequence(), Sequence(i64::MIN));
1583 }
1584
1585 // -- BorrowTracker tests (debug builds only) ------------------------------
1586
1587 #[cfg(debug_assertions)]
1588 #[test]
1589 #[should_panic(expected = "conflicting access")]
1590 fn borrow_tracker_catches_double_access() {
1591 let mut builder = WorldBuilder::new();
1592 let id = builder.register::<u64>(42);
1593 let world = builder.build();
1594 world.clear_borrows();
1595 world.track_borrow(id);
1596 world.track_borrow(id); // same resource, same phase
1597 }
1598
1599 #[cfg(debug_assertions)]
1600 #[test]
1601 fn borrow_tracker_allows_after_clear() {
1602 let mut builder = WorldBuilder::new();
1603 let id = builder.register::<u64>(42);
1604 let world = builder.build();
1605 world.clear_borrows();
1606 world.track_borrow(id);
1607 world.clear_borrows();
1608 world.track_borrow(id); // new phase, no conflict
1609 }
1610
1611 #[cfg(debug_assertions)]
1612 #[test]
1613 fn borrow_tracker_different_resources_ok() {
1614 let mut builder = WorldBuilder::new();
1615 let id_a = builder.register::<u64>(1);
1616 let id_b = builder.register::<u32>(2);
1617 let world = builder.build();
1618 world.clear_borrows();
1619 world.track_borrow(id_a);
1620 world.track_borrow(id_b); // different resources, no conflict
1621 }
1622}