nexus_rt/world.rs
1//! Type-erased singleton resource storage.
2//!
3//! [`World`] is a unified store where each resource type gets a dense index
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 dense indices. It is shared between
9//! [`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, UnsafeCell};
31use std::marker::PhantomData;
32
33use rustc_hash::FxHashMap;
34
35// =============================================================================
36// Debug-mode aliasing detection
37// =============================================================================
38
39/// Tracks resource accesses within a single Param::fetch phase to detect
40/// aliasing violations at runtime in debug builds.
41///
42/// Dispatch macros call [`World::clear_borrows`] then
43/// [`World::track_borrow`] for each resource fetched. If the same resource
44/// is fetched twice within a phase, we panic with a diagnostic. This catches
45/// framework bugs where two params in the same handler resolve to the same
46/// resource — something [`Registry::check_access`] catches at construction
47/// time, but this catches dynamically for dispatch paths (like `.switch()`
48/// closures) that bypass static analysis.
49///
50/// Only active during the narrow `Param::fetch` window — safe API methods
51/// and driver code that call `get_ptr` directly are not tracked.
52///
53/// Completely compiled out in release builds — zero bytes, zero branches.
54#[cfg(debug_assertions)]
55pub(crate) struct BorrowTracker {
56 /// One slot per resource. `true` = accessed in current phase.
57 /// Uses `UnsafeCell` for interior mutability because `Param::fetch` /
58 /// `World::track_borrow` operate on `&World`. Single-threaded,
59 /// non-reentrant access only.
60 accessed: UnsafeCell<Vec<bool>>,
61}
62
63#[cfg(debug_assertions)]
64impl BorrowTracker {
65 fn new(count: usize) -> Self {
66 Self {
67 accessed: UnsafeCell::new(vec![false; count]),
68 }
69 }
70
71 /// Reset all tracking state. Called before each `Param::fetch` phase.
72 fn clear(&self) {
73 // SAFETY: single-threaded, non-reentrant. No other reference to
74 // the inner Vec exists during this call.
75 let slots = unsafe { &mut *self.accessed.get() };
76 slots.fill(false);
77 }
78
79 /// Record an access. Panics if already accessed in this phase.
80 fn track(&self, id: ResourceId) {
81 // SAFETY: single-threaded, non-reentrant. No other reference to
82 // the inner Vec exists during this call.
83 let slots = unsafe { &mut *self.accessed.get() };
84 let idx = id.0 as usize;
85 assert!(
86 idx < slots.len(),
87 "invalid ResourceId {id}: index {idx} out of bounds \
88 for BorrowTracker (len = {})",
89 slots.len(),
90 );
91 assert!(
92 !slots[idx],
93 "conflicting access: resource {id} was accessed by more than one parameter \
94 in the same dispatch phase",
95 );
96 slots[idx] = true;
97 }
98}
99
100// =============================================================================
101// Core types
102// =============================================================================
103
104/// Dense index identifying a resource type within a [`World`] container.
105///
106/// Assigned sequentially at registration (0, 1, 2, ...). Used as a direct
107/// index into internal storage at dispatch time — no hashing, no searching.
108///
109/// Obtained from [`WorldBuilder::register`], [`WorldBuilder::ensure`],
110/// [`Registry::id`], [`World::id`], or their `try_` / `_default` variants.
111#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
112pub struct ResourceId(u16);
113
114impl std::fmt::Display for ResourceId {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 self.0.fmt(f)
117 }
118}
119
120/// Monotonic event sequence number for change detection.
121///
122/// Each event processed by a driver is assigned a unique sequence number
123/// via [`World::next_sequence`]. Resources record the sequence at which
124/// they were last written. A resource is considered "changed" if its
125/// `changed_at` equals the world's `current_sequence`. Wrapping is
126/// harmless — only equality is checked.
127#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
128pub struct Sequence(pub(crate) u64);
129
130impl std::fmt::Display for Sequence {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 self.0.fmt(f)
133 }
134}
135
136/// Type-erased drop function. Monomorphized at registration time so we
137/// can reconstruct and drop the original `Box<T>` from a `*mut u8`.
138pub(crate) type DropFn = unsafe fn(*mut u8);
139
140/// Reconstruct and drop a `Box<T>` from a raw pointer.
141///
142/// # Safety
143///
144/// `ptr` must have been produced by `Box::into_raw(Box::new(value))`
145/// where `value: T`. Must only be called once per pointer.
146pub(crate) unsafe fn drop_resource<T>(ptr: *mut u8) {
147 // SAFETY: ptr was produced by Box::into_raw(Box::new(value))
148 // where value: T. Called exactly once in Storage::drop.
149 unsafe {
150 let _ = Box::from_raw(ptr as *mut T);
151 }
152}
153
154// =============================================================================
155// Registry — type-to-index mapping
156// =============================================================================
157
158/// Type-to-index mapping shared between [`WorldBuilder`] and [`World`].
159///
160/// Contains only the type registry — no storage, no pointers. Passed to
161/// [`IntoHandler::into_handler`](crate::IntoHandler::into_handler) and
162/// [`Param::init`](crate::Param::init) so handlers can resolve
163/// [`ResourceId`]s during driver setup.
164///
165/// Obtained via [`WorldBuilder::registry()`] or [`World::registry()`].
166pub struct Registry {
167 indices: FxHashMap<TypeId, ResourceId>,
168 /// Scratch bitset reused across [`check_access`](Self::check_access) calls.
169 /// Allocated once on the first call with >128 resources, then reused.
170 ///
171 /// Interior mutability via `UnsafeCell` — only accessed in `check_access`,
172 /// which is single-threaded and non-reentrant. `UnsafeCell` makes
173 /// `Registry` `!Sync` automatically, which is correct — `World` is
174 /// already `!Sync`.
175 scratch: UnsafeCell<Vec<u64>>,
176}
177
178impl Registry {
179 pub(crate) fn new() -> Self {
180 Self {
181 indices: FxHashMap::default(),
182 scratch: UnsafeCell::new(Vec::new()),
183 }
184 }
185
186 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
187 ///
188 /// # Panics
189 ///
190 /// Panics if the resource type was not registered.
191 pub fn id<T: 'static>(&self) -> ResourceId {
192 *self
193 .indices
194 .get(&TypeId::of::<T>())
195 .unwrap_or_else(|| panic!("resource `{}` not registered", type_name::<T>()))
196 }
197
198 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
199 /// type was not registered.
200 pub fn try_id<T: 'static>(&self) -> Option<ResourceId> {
201 self.indices.get(&TypeId::of::<T>()).copied()
202 }
203
204 /// Returns `true` if a resource of type `T` has been registered.
205 pub fn contains<T: 'static>(&self) -> bool {
206 self.indices.contains_key(&TypeId::of::<T>())
207 }
208
209 /// Returns the number of registered resources.
210 pub fn len(&self) -> usize {
211 self.indices.len()
212 }
213
214 /// Returns `true` if no resources have been registered.
215 pub fn is_empty(&self) -> bool {
216 self.indices.is_empty()
217 }
218
219 /// Validate that a set of parameter accesses don't conflict.
220 ///
221 /// Two accesses conflict when they target the same ResourceId.
222 /// Called at construction time by `into_handler`, `into_callback`,
223 /// and `into_step`.
224 ///
225 /// Fast path (≤128 resources): single `u128` on the stack, zero heap.
226 /// Slow path (>128 resources): reusable `Vec<u64>` owned by Registry —
227 /// allocated once on first use, then cleared and reused.
228 ///
229 /// # Panics
230 ///
231 /// Panics if any resource is accessed by more than one parameter.
232 #[cold]
233 pub fn check_access(&self, accesses: &[(Option<ResourceId>, &str)]) {
234 let n = self.len();
235 if n == 0 {
236 return;
237 }
238
239 if n <= 128 {
240 // Fast path: single u128 on the stack.
241 let mut seen = 0u128;
242 for &(id, name) in accesses {
243 let Some(id) = id else { continue };
244 let bit = 1u128 << id.0 as u32;
245 assert!(
246 seen & bit == 0,
247 "conflicting access: resource borrowed by `{}` is already \
248 borrowed by another parameter in the same handler",
249 name,
250 );
251 seen |= bit;
252 }
253 } else {
254 // Slow path: reusable heap buffer.
255 // SAFETY: single-threaded access guaranteed by !Sync on Registry
256 // (UnsafeCell is !Sync). check_access is non-reentrant — it runs,
257 // uses scratch, and returns. No aliasing possible.
258 let scratch = unsafe { &mut *self.scratch.get() };
259 let words = n.div_ceil(64);
260 scratch.resize(words, 0);
261 scratch.fill(0);
262 for &(id, name) in accesses {
263 let Some(id) = id else { continue };
264 let word = id.0 as usize / 64;
265 let bit = 1u64 << (id.0 as u32 % 64);
266 assert!(
267 scratch[word] & bit == 0,
268 "conflicting access: resource borrowed by `{}` is already \
269 borrowed by another parameter in the same handler",
270 name,
271 );
272 scratch[word] |= bit;
273 }
274 }
275 }
276}
277
278// =============================================================================
279// Storage — shared backing between builder and frozen container
280// =============================================================================
281
282/// Interleaved pointer + change sequence for a single resource.
283/// 16 bytes — 4 slots per cache line.
284#[repr(C)]
285pub(crate) struct ResourceSlot {
286 pub(crate) ptr: *mut u8,
287 pub(crate) changed_at: Cell<Sequence>,
288}
289
290/// Internal storage for type-erased resource pointers and their destructors.
291///
292/// Owns the heap allocations and is responsible for cleanup. Shared between
293/// [`WorldBuilder`] and [`World`] via move — avoids duplicating Drop logic.
294pub(crate) struct Storage {
295 /// Dense array of interleaved pointer + change sequence pairs.
296 /// Each pointer was produced by `Box::into_raw`.
297 pub(crate) slots: Vec<ResourceSlot>,
298 /// Parallel array of drop functions. `drop_fns[i]` is the monomorphized
299 /// destructor for the concrete type behind `slots[i].ptr`.
300 pub(crate) drop_fns: Vec<DropFn>,
301}
302
303impl Storage {
304 pub(crate) fn new() -> Self {
305 Self {
306 slots: Vec::new(),
307 drop_fns: Vec::new(),
308 }
309 }
310
311 pub(crate) fn len(&self) -> usize {
312 self.slots.len()
313 }
314
315 pub(crate) fn is_empty(&self) -> bool {
316 self.slots.is_empty()
317 }
318}
319
320// SAFETY: All values stored in Storage were registered via `register<T: Send + 'static>`,
321// so every concrete type behind the raw pointers is Send. Storage exclusively owns
322// these heap allocations — they are not aliased or shared. Transferring ownership
323// to another thread is safe. Cell<Sequence> is !Sync but we're transferring
324// ownership, not sharing.
325#[allow(clippy::non_send_fields_in_send_ty)]
326unsafe impl Send for Storage {}
327
328impl Drop for Storage {
329 fn drop(&mut self) {
330 for (slot, drop_fn) in self.slots.iter().zip(&self.drop_fns) {
331 // SAFETY: each (slot.ptr, drop_fn) pair was created together in
332 // WorldBuilder::register(). drop_fn is the monomorphized
333 // destructor for the concrete type behind ptr. Called exactly
334 // once here.
335 unsafe {
336 drop_fn(slot.ptr);
337 }
338 }
339 }
340}
341
342// =============================================================================
343// WorldBuilder
344// =============================================================================
345
346/// Builder for registering resources before freezing into a [`World`] container.
347///
348/// Each resource type can only be registered once. Registration assigns a
349/// dense [`ResourceId`] index (0, 1, 2, ...).
350///
351/// The [`registry()`](Self::registry) method exposes the type-to-index mapping
352/// so that drivers can resolve handlers against the builder before `build()`.
353///
354/// # Examples
355///
356/// ```
357/// use nexus_rt::WorldBuilder;
358///
359/// let mut builder = WorldBuilder::new();
360/// let id = builder.register::<u64>(42);
361/// builder.register::<bool>(true);
362/// let world = builder.build();
363///
364/// unsafe {
365/// assert_eq!(*world.get::<u64>(id), 42);
366/// }
367/// ```
368pub struct WorldBuilder {
369 registry: Registry,
370 storage: Storage,
371}
372
373impl WorldBuilder {
374 /// Create an empty builder.
375 pub fn new() -> Self {
376 Self {
377 registry: Registry::new(),
378 storage: Storage::new(),
379 }
380 }
381
382 /// Register a resource and return its [`ResourceId`].
383 ///
384 /// The value is heap-allocated via `Box` and ownership is transferred
385 /// to the container. The pointer is stable for the lifetime of the
386 /// resulting [`World`].
387 ///
388 /// # Panics
389 ///
390 /// Panics if a resource of the same type is already registered.
391 #[cold]
392 pub fn register<T: Send + 'static>(&mut self, value: T) -> ResourceId {
393 let type_id = TypeId::of::<T>();
394 assert!(
395 !self.registry.indices.contains_key(&type_id),
396 "resource `{}` already registered",
397 type_name::<T>(),
398 );
399
400 assert!(
401 u16::try_from(self.storage.slots.len()).is_ok(),
402 "resource limit exceeded ({} registered, max {})",
403 self.storage.slots.len(),
404 usize::from(u16::MAX) + 1,
405 );
406
407 let ptr = Box::into_raw(Box::new(value)) as *mut u8;
408 let id = ResourceId(self.storage.slots.len() as u16);
409 self.registry.indices.insert(type_id, id);
410 self.storage.slots.push(ResourceSlot {
411 ptr,
412 changed_at: Cell::new(Sequence(0)),
413 });
414 self.storage.drop_fns.push(drop_resource::<T>);
415 id
416 }
417
418 /// Register a resource using its [`Default`] value and return its
419 /// [`ResourceId`].
420 ///
421 /// Equivalent to `self.register::<T>(T::default())`.
422 #[cold]
423 pub fn register_default<T: Default + Send + 'static>(&mut self) -> ResourceId {
424 self.register(T::default())
425 }
426
427 /// Ensure a resource is registered, returning its [`ResourceId`].
428 ///
429 /// If the type is already registered, returns the existing ID and
430 /// drops `value`. If not, registers it and returns the new ID.
431 ///
432 /// Use [`register`](Self::register) when duplicate registration is a
433 /// bug that should panic. Use `ensure` when multiple plugins or
434 /// drivers may independently need the same resource type.
435 #[cold]
436 pub fn ensure<T: Send + 'static>(&mut self, value: T) -> ResourceId {
437 if let Some(id) = self.registry.try_id::<T>() {
438 return id;
439 }
440 self.register(value)
441 }
442
443 /// Ensure a resource is registered using its [`Default`] value,
444 /// returning its [`ResourceId`].
445 ///
446 /// If the type is already registered, returns the existing ID.
447 /// If not, registers `T::default()` and returns the new ID.
448 #[cold]
449 pub fn ensure_default<T: Default + Send + 'static>(&mut self) -> ResourceId {
450 if let Some(id) = self.registry.try_id::<T>() {
451 return id;
452 }
453 self.register(T::default())
454 }
455
456 /// Returns a shared reference to the type registry.
457 ///
458 /// Use this for construction-time calls like
459 /// [`into_handler`](crate::IntoHandler::into_handler),
460 /// [`into_callback`](crate::IntoCallback::into_callback), and
461 /// [`into_step`](crate::IntoStep::into_step).
462 pub fn registry(&self) -> &Registry {
463 &self.registry
464 }
465
466 /// Returns a mutable reference to the type registry.
467 ///
468 /// Rarely needed — [`registry()`](Self::registry) suffices for
469 /// construction-time calls. Exists for direct mutation of the
470 /// registry if needed.
471 pub fn registry_mut(&mut self) -> &mut Registry {
472 &mut self.registry
473 }
474
475 /// Returns the number of registered resources.
476 pub fn len(&self) -> usize {
477 self.storage.len()
478 }
479
480 /// Returns `true` if no resources have been registered.
481 pub fn is_empty(&self) -> bool {
482 self.storage.is_empty()
483 }
484
485 /// Returns `true` if a resource of type `T` has been registered.
486 pub fn contains<T: 'static>(&self) -> bool {
487 self.registry.contains::<T>()
488 }
489
490 /// Install a plugin. The plugin is consumed and registers its
491 /// resources into this builder.
492 pub fn install_plugin(&mut self, plugin: impl crate::plugin::Plugin) -> &mut Self {
493 plugin.build(self);
494 self
495 }
496
497 /// Install a driver. The installer is consumed, registers its resources
498 /// into this builder, and returns a concrete poller for dispatch-time
499 /// polling.
500 pub fn install_driver<D: crate::driver::Installer>(&mut self, driver: D) -> D::Poller {
501 driver.install(self)
502 }
503
504 /// Freeze the builder into an immutable [`World`] container.
505 ///
506 /// Automatically registers a [`Shutdown`](crate::shutdown::Shutdown)
507 /// resource if one wasn't already registered. After this call, no
508 /// more resources can be registered. All [`ResourceId`] values
509 /// remain valid for the lifetime of the returned [`World`].
510 pub fn build(mut self) -> World {
511 self.ensure(crate::shutdown::Shutdown::new());
512 #[cfg(debug_assertions)]
513 let resource_count = self.storage.len();
514 World {
515 registry: self.registry,
516 storage: self.storage,
517 current_sequence: Sequence(0),
518 _not_sync: PhantomData,
519 #[cfg(debug_assertions)]
520 borrow_tracker: BorrowTracker::new(resource_count),
521 }
522 }
523}
524
525impl Default for WorldBuilder {
526 fn default() -> Self {
527 Self::new()
528 }
529}
530
531// =============================================================================
532// World — frozen container
533// =============================================================================
534
535/// Frozen singleton resource storage.
536///
537/// Analogous to Bevy's `World`, but restricted to singleton resources
538/// (no entities, no components, no archetypes).
539///
540/// Created by [`WorldBuilder::build()`]. Resources are indexed by dense
541/// [`ResourceId`] for O(1) dispatch-time access (~3 cycles per fetch).
542///
543/// # Safe API
544///
545/// - [`resource`](Self::resource) / [`resource_mut`](Self::resource_mut) —
546/// cold-path access via HashMap lookup.
547///
548/// # Unsafe API (framework internals)
549///
550/// The low-level `get` / `get_mut` methods are `unsafe` — used by
551/// [`Param::fetch`](crate::Param) for ~3-cycle dispatch.
552/// The caller must ensure no mutable aliasing.
553pub struct World {
554 /// Type-to-index mapping. Same registry used during build.
555 registry: Registry,
556 /// Type-erased pointer storage. Drop handled by `Storage`.
557 storage: Storage,
558 /// Current sequence number. Advanced by the driver before
559 /// each event dispatch.
560 current_sequence: Sequence,
561 /// World must not be shared across threads — it holds interior-mutable
562 /// `Cell<Sequence>` values accessed through `&self`. `!Sync` enforced by
563 /// `PhantomData<Cell<()>>`.
564 _not_sync: PhantomData<Cell<()>>,
565 /// Debug-only aliasing tracker. Detects duplicate resource access within
566 /// a single dispatch phase. Compiled out entirely in release builds.
567 #[cfg(debug_assertions)]
568 borrow_tracker: BorrowTracker,
569}
570
571impl World {
572 /// Convenience constructor — returns a new [`WorldBuilder`].
573 pub fn builder() -> WorldBuilder {
574 WorldBuilder::new()
575 }
576
577 /// Returns a shared reference to the type registry.
578 ///
579 /// Use this for read-only queries (e.g. [`id`](Registry::id),
580 /// [`contains`](Registry::contains)) and construction-time calls
581 /// like [`into_handler`](crate::IntoHandler::into_handler).
582 pub fn registry(&self) -> &Registry {
583 &self.registry
584 }
585
586 /// Returns a mutable reference to the type registry.
587 ///
588 /// Rarely needed — [`registry()`](Self::registry) suffices for
589 /// construction-time calls. Exists for direct mutation of the
590 /// registry if needed.
591 pub fn registry_mut(&mut self) -> &mut Registry {
592 &mut self.registry
593 }
594
595 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
596 ///
597 /// # Panics
598 ///
599 /// Panics if the resource type was not registered.
600 pub fn id<T: 'static>(&self) -> ResourceId {
601 self.registry.id::<T>()
602 }
603
604 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
605 /// type was not registered.
606 pub fn try_id<T: 'static>(&self) -> Option<ResourceId> {
607 self.registry.try_id::<T>()
608 }
609
610 /// Returns the number of registered resources.
611 pub fn len(&self) -> usize {
612 self.storage.len()
613 }
614
615 /// Returns `true` if no resources are stored.
616 pub fn is_empty(&self) -> bool {
617 self.storage.is_empty()
618 }
619
620 /// Returns `true` if a resource of type `T` is stored.
621 pub fn contains<T: 'static>(&self) -> bool {
622 self.registry.contains::<T>()
623 }
624
625 // =========================================================================
626 // Safe resource access (cold path — HashMap lookup per call)
627 // =========================================================================
628
629 /// Safe shared access to a resource. Cold path — resolves via HashMap.
630 ///
631 /// Takes `&self` — multiple shared references can coexist. The borrow
632 /// checker prevents mixing with [`resource_mut`](Self::resource_mut)
633 /// (which takes `&mut self`).
634 ///
635 /// # Panics
636 ///
637 /// Panics if the resource type was not registered.
638 pub fn resource<T: 'static>(&self) -> &T {
639 let id = self.registry.id::<T>();
640 // SAFETY: id resolved from our own registry. &self prevents mutable
641 // aliases — resource_mut takes &mut self.
642 unsafe { self.get(id) }
643 }
644
645 /// Safe exclusive access to a resource. Cold path — resolves via HashMap.
646 ///
647 /// # Panics
648 ///
649 /// Panics if the resource type was not registered.
650 pub fn resource_mut<T: 'static>(&mut self) -> &mut T {
651 let id = self.registry.id::<T>();
652 // Cold path — stamp unconditionally. If you request &mut, you're writing.
653 self.storage.slots[id.0 as usize]
654 .changed_at
655 .set(self.current_sequence);
656 // SAFETY: id resolved from our own registry. &mut self ensures
657 // exclusive access — no other references can exist.
658 unsafe { self.get_mut(id) }
659 }
660
661 // =========================================================================
662 // One-shot dispatch
663 // =========================================================================
664
665 /// Run a handler once with full Param resolution.
666 ///
667 /// Intended for one-shot initialization after [`build()`](WorldBuilder::build).
668 /// The handler receives `()` as the event — the event parameter is
669 /// discarded. Named functions only (same closure limitation as
670 /// [`IntoHandler`](crate::IntoHandler)).
671 ///
672 /// Can be called multiple times for phased initialization.
673 ///
674 /// # Examples
675 ///
676 /// ```ignore
677 /// fn startup(
678 /// mut driver: ResMut<MioDriver>,
679 /// mut listener: ResMut<Listener>,
680 /// _event: (),
681 /// ) {
682 /// // wire drivers to IO sources...
683 /// }
684 ///
685 /// let mut world = wb.build();
686 /// world.run_startup(startup);
687 /// ```
688 pub fn run_startup<F, Params>(&mut self, f: F)
689 where
690 F: crate::IntoHandler<(), Params>,
691 {
692 use crate::Handler;
693 let mut handler = f.into_handler(&self.registry);
694 handler.run(self, ());
695 }
696
697 // =========================================================================
698 // Shutdown
699 // =========================================================================
700
701 /// Returns a [`ShutdownHandle`](crate::shutdown::ShutdownHandle)
702 /// sharing the same flag as the [`Shutdown`](crate::shutdown::Shutdown)
703 /// resource.
704 ///
705 /// The handle is owned by the event loop and checked each iteration.
706 /// Handlers trigger shutdown via `Res<Shutdown>::shutdown()`.
707 pub fn shutdown_handle(&self) -> crate::shutdown::ShutdownHandle {
708 self.resource::<crate::shutdown::Shutdown>().handle()
709 }
710
711 /// Run the event loop until shutdown is triggered.
712 ///
713 /// The closure receives `&mut World` and defines one iteration of
714 /// the poll loop — which drivers to poll, in what order, with what
715 /// timeout. The loop exits when a handler calls
716 /// [`Shutdown::shutdown`](crate::shutdown::Shutdown::shutdown) or
717 /// an external signal flips the flag (see
718 /// [`ShutdownHandle::enable_signals`](crate::shutdown::ShutdownHandle::enable_signals)).
719 ///
720 /// The shutdown flag is resolved once before entering the loop —
721 /// no resource lookup per iteration.
722 ///
723 /// # Examples
724 ///
725 /// ```ignore
726 /// let mut world = wb.build();
727 /// world.run_startup(startup);
728 ///
729 /// world.run(|world| {
730 /// let now = Instant::now();
731 /// let timeout = timer.next_deadline(world)
732 /// .map(|d| d.saturating_duration_since(now));
733 /// mio.poll(world, timeout).expect("mio poll");
734 /// timer.poll(world, now);
735 /// });
736 /// ```
737 pub fn run(&mut self, mut f: impl FnMut(&mut World)) {
738 let flag = self.resource::<crate::shutdown::Shutdown>().flag();
739 while !flag.load(std::sync::atomic::Ordering::Relaxed) {
740 f(self);
741 }
742 }
743
744 // =========================================================================
745 // Sequence / change detection
746 // =========================================================================
747
748 /// Returns the current event sequence number.
749 pub fn current_sequence(&self) -> Sequence {
750 self.current_sequence
751 }
752
753 /// Advance to the next event sequence number and return it.
754 ///
755 /// Drivers call this before dispatching each event. The returned
756 /// sequence number identifies the event being processed. Resources
757 /// mutated during dispatch will record this sequence in `changed_at`.
758 pub fn next_sequence(&mut self) -> Sequence {
759 self.current_sequence = Sequence(self.current_sequence.0.wrapping_add(1));
760 self.current_sequence
761 }
762
763 // =========================================================================
764 // Unsafe resource access (hot path — pre-resolved ResourceId)
765 // =========================================================================
766
767 /// Fetch a shared reference to a resource by pre-validated index.
768 ///
769 /// # Safety
770 ///
771 /// - `id` must have been returned by [`WorldBuilder::register`] for
772 /// the same builder that produced this container.
773 /// - `T` must be the same type that was registered at this `id`.
774 /// - The caller must ensure no mutable reference to this resource exists.
775 #[inline(always)]
776 pub unsafe fn get<T: 'static>(&self, id: ResourceId) -> &T {
777 // SAFETY: caller guarantees id was returned by register() on the
778 // builder that produced this container, so id.0 < self.storage.slots.len().
779 // T matches the registered type. No mutable alias exists.
780 unsafe { &*(self.get_ptr(id) as *const T) }
781 }
782
783 /// Fetch a mutable reference to a resource by pre-validated index.
784 ///
785 /// Takes `&self` — the container structure is frozen, but individual
786 /// resources have interior mutability via raw pointers. Sound because
787 /// callers (single-threaded sequential dispatch) uphold no-aliasing.
788 ///
789 /// # Safety
790 ///
791 /// - `id` must have been returned by [`WorldBuilder::register`] for
792 /// the same builder that produced this container.
793 /// - `T` must be the same type that was registered at this `id`.
794 /// - The caller must ensure no other reference (shared or mutable) to this
795 /// resource exists.
796 #[inline(always)]
797 #[allow(clippy::mut_from_ref)]
798 pub unsafe fn get_mut<T: 'static>(&self, id: ResourceId) -> &mut T {
799 // SAFETY: caller guarantees id was returned by register() on the
800 // builder that produced this container, so id.0 < self.storage.slots.len().
801 // T matches the registered type. No aliases exist.
802 unsafe { &mut *(self.get_ptr(id) as *mut T) }
803 }
804
805 /// Reset borrow tracking for a new dispatch phase.
806 ///
807 /// Called before each [`Param::fetch`](crate::Param::fetch) in dispatch
808 /// macros. Only exists in debug builds.
809 #[cfg(debug_assertions)]
810 pub(crate) fn clear_borrows(&self) {
811 self.borrow_tracker.clear();
812 }
813
814 /// Record a resource access in the debug borrow tracker.
815 ///
816 /// Called by [`Param::fetch`](crate::Param::fetch) impls for each
817 /// resource parameter. Panics if the resource was already accessed
818 /// in the current phase (since the last [`clear_borrows`](Self::clear_borrows)).
819 /// Only exists in debug builds.
820 #[cfg(debug_assertions)]
821 pub(crate) fn track_borrow(&self, id: ResourceId) {
822 self.borrow_tracker.track(id);
823 }
824
825 /// Fetch a raw pointer to a resource by pre-validated index.
826 ///
827 /// Intended for macro-generated dispatch code that needs direct pointer
828 /// access.
829 ///
830 /// # Safety
831 ///
832 /// - `id` must have been returned by [`WorldBuilder::register`] for
833 /// the same builder that produced this container.
834 #[inline(always)]
835 pub unsafe fn get_ptr(&self, id: ResourceId) -> *mut u8 {
836 debug_assert!(
837 (id.0 as usize) < self.storage.slots.len(),
838 "ResourceId({}) out of bounds (len {})",
839 id.0,
840 self.storage.slots.len(),
841 );
842 // SAFETY: caller guarantees id was returned by register() on the
843 // builder that produced this container, so id.0 < self.storage.slots.len().
844 unsafe { self.storage.slots.get_unchecked(id.0 as usize).ptr }
845 }
846
847 // =========================================================================
848 // Change-detection internals (framework use only)
849 // =========================================================================
850
851 /// Read the sequence at which a resource was last changed.
852 ///
853 /// # Safety
854 ///
855 /// `id` must have been returned by [`WorldBuilder::register`] for
856 /// the same builder that produced this container.
857 #[inline(always)]
858 pub(crate) unsafe fn changed_at(&self, id: ResourceId) -> Sequence {
859 unsafe {
860 self.storage
861 .slots
862 .get_unchecked(id.0 as usize)
863 .changed_at
864 .get()
865 }
866 }
867
868 /// Get a reference to the `Cell` tracking a resource's change sequence.
869 ///
870 /// # Safety
871 ///
872 /// `id` must have been returned by [`WorldBuilder::register`] for
873 /// the same builder that produced this container.
874 #[inline(always)]
875 pub(crate) unsafe fn changed_at_cell(&self, id: ResourceId) -> &Cell<Sequence> {
876 unsafe { &self.storage.slots.get_unchecked(id.0 as usize).changed_at }
877 }
878
879 /// Stamp a resource as changed at the current sequence.
880 ///
881 /// # Safety
882 ///
883 /// `id` must have been returned by [`WorldBuilder::register`] for
884 /// the same builder that produced this container.
885 #[inline(always)]
886 #[allow(dead_code)] // Available for driver implementations.
887 pub(crate) unsafe fn stamp_changed(&self, id: ResourceId) {
888 unsafe {
889 self.storage
890 .slots
891 .get_unchecked(id.0 as usize)
892 .changed_at
893 .set(self.current_sequence);
894 }
895 }
896}
897
898// SAFETY: All resources are `T: Send` (enforced by `register`). World owns all
899// heap-allocated data exclusively — the raw pointers are not aliased or shared.
900// Transferring ownership to another thread is safe; the new thread becomes the
901// sole accessor.
902unsafe impl Send for World {}
903
904// =============================================================================
905// Tests
906// =============================================================================
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911 use std::sync::{Arc, Weak};
912
913 struct Price {
914 value: f64,
915 }
916
917 struct Venue {
918 name: &'static str,
919 }
920
921 struct Config {
922 max_orders: usize,
923 }
924
925 #[test]
926 fn register_and_build() {
927 let mut builder = WorldBuilder::new();
928 builder.register::<Price>(Price { value: 100.0 });
929 builder.register::<Venue>(Venue { name: "test" });
930 let world = builder.build();
931 // +1 for auto-registered Shutdown.
932 assert_eq!(world.len(), 3);
933 }
934
935 #[test]
936 fn resource_ids_are_sequential() {
937 let mut builder = WorldBuilder::new();
938 let id0 = builder.register::<Price>(Price { value: 0.0 });
939 let id1 = builder.register::<Venue>(Venue { name: "" });
940 let id2 = builder.register::<Config>(Config { max_orders: 0 });
941 assert_eq!(id0, ResourceId(0));
942 assert_eq!(id1, ResourceId(1));
943 assert_eq!(id2, ResourceId(2));
944 }
945
946 #[test]
947 fn get_returns_registered_value() {
948 let mut builder = WorldBuilder::new();
949 builder.register::<Price>(Price { value: 42.5 });
950 let world = builder.build();
951
952 let id = world.id::<Price>();
953 // SAFETY: id resolved from this container, type matches, no aliasing.
954 let price = unsafe { world.get::<Price>(id) };
955 assert_eq!(price.value, 42.5);
956 }
957
958 #[test]
959 fn get_mut_modifies_value() {
960 let mut builder = WorldBuilder::new();
961 builder.register::<Price>(Price { value: 1.0 });
962 let world = builder.build();
963
964 let id = world.id::<Price>();
965 // SAFETY: id resolved from this container, type matches, no aliasing.
966 unsafe {
967 world.get_mut::<Price>(id).value = 99.0;
968 assert_eq!(world.get::<Price>(id).value, 99.0);
969 }
970 }
971
972 #[test]
973 fn try_id_returns_none_for_unregistered() {
974 let world = WorldBuilder::new().build();
975 assert!(world.try_id::<Price>().is_none());
976 }
977
978 #[test]
979 fn try_id_returns_some_for_registered() {
980 let mut builder = WorldBuilder::new();
981 builder.register::<Price>(Price { value: 0.0 });
982 let world = builder.build();
983
984 assert!(world.try_id::<Price>().is_some());
985 }
986
987 #[test]
988 #[should_panic(expected = "already registered")]
989 fn panics_on_duplicate_registration() {
990 let mut builder = WorldBuilder::new();
991 builder.register::<Price>(Price { value: 1.0 });
992 builder.register::<Price>(Price { value: 2.0 });
993 }
994
995 #[test]
996 #[should_panic(expected = "not registered")]
997 fn panics_on_unregistered_id() {
998 let world = WorldBuilder::new().build();
999 world.id::<Price>();
1000 }
1001
1002 #[test]
1003 fn empty_builder_builds_empty_world() {
1004 let world = WorldBuilder::new().build();
1005 // Shutdown is auto-registered by build().
1006 assert_eq!(world.len(), 1);
1007 assert!(world.contains::<crate::shutdown::Shutdown>());
1008 }
1009
1010 #[test]
1011 fn drop_runs_destructors() {
1012 let arc = Arc::new(42u32);
1013 let weak: Weak<u32> = Arc::downgrade(&arc);
1014
1015 {
1016 let mut builder = WorldBuilder::new();
1017 builder.register::<Arc<u32>>(arc);
1018 let _world = builder.build();
1019 // Arc still alive — held by World
1020 assert!(weak.upgrade().is_some());
1021 }
1022 // World dropped — Arc should be deallocated
1023 assert!(weak.upgrade().is_none());
1024 }
1025
1026 #[test]
1027 fn builder_drop_cleans_up_without_build() {
1028 let arc = Arc::new(99u32);
1029 let weak: Weak<u32> = Arc::downgrade(&arc);
1030
1031 {
1032 let mut builder = WorldBuilder::new();
1033 builder.register::<Arc<u32>>(arc);
1034 }
1035 // Builder dropped without build() — Storage::drop cleans up
1036 assert!(weak.upgrade().is_none());
1037 }
1038
1039 #[test]
1040 fn multiple_types_independent() {
1041 let mut builder = WorldBuilder::new();
1042 let price_id = builder.register::<Price>(Price { value: 10.0 });
1043 let venue_id = builder.register::<Venue>(Venue { name: "CB" });
1044 let config_id = builder.register::<Config>(Config { max_orders: 500 });
1045 let world = builder.build();
1046
1047 unsafe {
1048 assert_eq!(world.get::<Price>(price_id).value, 10.0);
1049 assert_eq!(world.get::<Venue>(venue_id).name, "CB");
1050 assert_eq!(world.get::<Config>(config_id).max_orders, 500);
1051 }
1052 }
1053
1054 #[test]
1055 fn contains_reflects_registration() {
1056 let mut builder = WorldBuilder::new();
1057 assert!(!builder.contains::<Price>());
1058
1059 builder.register::<Price>(Price { value: 0.0 });
1060 assert!(builder.contains::<Price>());
1061 assert!(!builder.contains::<Venue>());
1062
1063 let world = builder.build();
1064 assert!(world.contains::<Price>());
1065 assert!(!world.contains::<Venue>());
1066 }
1067
1068 #[test]
1069 fn get_ptr_returns_valid_pointer() {
1070 let mut builder = WorldBuilder::new();
1071 builder.register::<Price>(Price { value: 77.7 });
1072 let world = builder.build();
1073
1074 let id = world.id::<Price>();
1075 unsafe {
1076 let ptr = world.get_ptr(id);
1077 let price = &*(ptr as *const Price);
1078 assert_eq!(price.value, 77.7);
1079 }
1080 }
1081
1082 #[test]
1083 fn send_to_another_thread() {
1084 let mut builder = WorldBuilder::new();
1085 builder.register::<Price>(Price { value: 55.5 });
1086 let world = builder.build();
1087
1088 let handle = std::thread::spawn(move || {
1089 let id = world.id::<Price>();
1090 // SAFETY: sole owner on this thread, no aliasing.
1091 unsafe { world.get::<Price>(id).value }
1092 });
1093 assert_eq!(handle.join().unwrap(), 55.5);
1094 }
1095
1096 #[test]
1097 fn registry_accessible_from_builder() {
1098 let mut builder = WorldBuilder::new();
1099 builder.register::<u64>(42);
1100
1101 let registry = builder.registry();
1102 assert!(registry.contains::<u64>());
1103 assert!(!registry.contains::<bool>());
1104
1105 let id = registry.id::<u64>();
1106 assert_eq!(id, ResourceId(0));
1107 }
1108
1109 #[test]
1110 fn registry_accessible_from_world() {
1111 let mut builder = WorldBuilder::new();
1112 builder.register::<u64>(42);
1113 let world = builder.build();
1114
1115 let registry = world.registry();
1116 assert!(registry.contains::<u64>());
1117
1118 // Registry from world and world.id() agree.
1119 assert_eq!(registry.id::<u64>(), world.id::<u64>());
1120 }
1121
1122 // -- Safe accessor tests --------------------------------------------------
1123
1124 #[test]
1125 fn resource_reads_value() {
1126 let mut builder = WorldBuilder::new();
1127 builder.register::<Price>(Price { value: 42.5 });
1128 let world = builder.build();
1129
1130 assert_eq!(world.resource::<Price>().value, 42.5);
1131 }
1132
1133 #[test]
1134 fn resource_mut_modifies_value() {
1135 let mut builder = WorldBuilder::new();
1136 builder.register::<u64>(0);
1137 let mut world = builder.build();
1138
1139 *world.resource_mut::<u64>() = 99;
1140 assert_eq!(*world.resource::<u64>(), 99);
1141 }
1142
1143 #[test]
1144 fn register_default_works() {
1145 let mut builder = WorldBuilder::new();
1146 let id = builder.register_default::<Vec<u32>>();
1147 let world = builder.build();
1148
1149 assert_eq!(id, world.id::<Vec<u32>>());
1150 let v = world.resource::<Vec<u32>>();
1151 assert!(v.is_empty());
1152 }
1153
1154 #[test]
1155 fn ensure_registers_new_type() {
1156 let mut builder = WorldBuilder::new();
1157 let id = builder.ensure::<u64>(42);
1158 let world = builder.build();
1159
1160 assert_eq!(id, world.id::<u64>());
1161 assert_eq!(*world.resource::<u64>(), 42);
1162 }
1163
1164 #[test]
1165 fn ensure_returns_existing_id() {
1166 let mut builder = WorldBuilder::new();
1167 let id1 = builder.register::<u64>(42);
1168 let id2 = builder.ensure::<u64>(99);
1169 assert_eq!(id1, id2);
1170
1171 // Original value preserved, new value dropped.
1172 let world = builder.build();
1173 assert_eq!(*world.resource::<u64>(), 42);
1174 }
1175
1176 #[test]
1177 fn ensure_default_registers_new_type() {
1178 let mut builder = WorldBuilder::new();
1179 let id = builder.ensure_default::<Vec<u32>>();
1180 let world = builder.build();
1181
1182 assert_eq!(id, world.id::<Vec<u32>>());
1183 assert!(world.resource::<Vec<u32>>().is_empty());
1184 }
1185
1186 #[test]
1187 fn ensure_default_returns_existing_id() {
1188 let mut builder = WorldBuilder::new();
1189 builder.register::<Vec<u32>>(vec![1, 2, 3]);
1190 let id = builder.ensure_default::<Vec<u32>>();
1191 let world = builder.build();
1192
1193 assert_eq!(id, world.id::<Vec<u32>>());
1194 // Original value preserved.
1195 assert_eq!(*world.resource::<Vec<u32>>(), vec![1, 2, 3]);
1196 }
1197
1198 // -- Sequence / change detection tests ----------------------------------------
1199
1200 #[test]
1201 fn sequence_default_is_zero() {
1202 assert_eq!(Sequence::default(), Sequence(0));
1203 }
1204
1205 #[test]
1206 fn next_sequence_increments() {
1207 let mut world = WorldBuilder::new().build();
1208 assert_eq!(world.current_sequence(), Sequence(0));
1209 world.next_sequence();
1210 assert_eq!(world.current_sequence(), Sequence(1));
1211 world.next_sequence();
1212 assert_eq!(world.current_sequence(), Sequence(2));
1213 }
1214
1215 #[test]
1216 fn resource_registered_at_current_sequence() {
1217 // Resources registered at build time get changed_at=Sequence(0).
1218 // World starts at current_sequence=Sequence(0). So they match — "changed."
1219 let mut builder = WorldBuilder::new();
1220 builder.register::<u64>(42);
1221 let world = builder.build();
1222
1223 let id = world.id::<u64>();
1224 unsafe {
1225 assert_eq!(world.changed_at(id), Sequence(0));
1226 assert_eq!(world.current_sequence(), Sequence(0));
1227 // changed_at == current_sequence → "changed"
1228 assert_eq!(world.changed_at(id), world.current_sequence());
1229 }
1230 }
1231
1232 #[test]
1233 fn resource_mut_stamps_changed_at() {
1234 let mut builder = WorldBuilder::new();
1235 builder.register::<u64>(0);
1236 let mut world = builder.build();
1237
1238 world.next_sequence(); // tick=1
1239 let id = world.id::<u64>();
1240
1241 // changed_at is still 0, current_sequence is 1 → not changed
1242 unsafe {
1243 assert_eq!(world.changed_at(id), Sequence(0));
1244 }
1245
1246 // resource_mut stamps changed_at to current_sequence
1247 *world.resource_mut::<u64>() = 99;
1248 unsafe {
1249 assert_eq!(world.changed_at(id), Sequence(1));
1250 }
1251 }
1252
1253 // -- run_startup tests ----------------------------------------------------
1254
1255 #[test]
1256 fn run_startup_dispatches_handler() {
1257 use crate::ResMut;
1258
1259 let mut builder = WorldBuilder::new();
1260 builder.register::<u64>(0);
1261 builder.register::<bool>(false);
1262 let mut world = builder.build();
1263
1264 fn init(mut counter: ResMut<u64>, mut flag: ResMut<bool>, _event: ()) {
1265 *counter = 42;
1266 *flag = true;
1267 }
1268
1269 world.run_startup(init);
1270
1271 assert_eq!(*world.resource::<u64>(), 42);
1272 assert!(*world.resource::<bool>());
1273 }
1274
1275 #[test]
1276 fn run_startup_multiple_phases() {
1277 use crate::ResMut;
1278
1279 let mut builder = WorldBuilder::new();
1280 builder.register::<u64>(0);
1281 let mut world = builder.build();
1282
1283 fn phase1(mut counter: ResMut<u64>, _event: ()) {
1284 *counter += 10;
1285 }
1286
1287 fn phase2(mut counter: ResMut<u64>, _event: ()) {
1288 *counter += 5;
1289 }
1290
1291 world.run_startup(phase1);
1292 world.run_startup(phase2);
1293
1294 assert_eq!(*world.resource::<u64>(), 15);
1295 }
1296
1297 // -- Plugin / Driver tests ------------------------------------------------
1298
1299 #[test]
1300 fn plugin_registers_resources() {
1301 struct TestPlugin;
1302
1303 impl crate::plugin::Plugin for TestPlugin {
1304 fn build(self, world: &mut WorldBuilder) {
1305 world.register::<u64>(42);
1306 world.register::<bool>(true);
1307 }
1308 }
1309
1310 let mut builder = WorldBuilder::new();
1311 builder.install_plugin(TestPlugin);
1312 let world = builder.build();
1313
1314 assert_eq!(*world.resource::<u64>(), 42);
1315 assert_eq!(*world.resource::<bool>(), true);
1316 }
1317
1318 #[test]
1319 fn driver_installs_and_returns_handle() {
1320 struct TestInstaller;
1321 struct TestHandle {
1322 counter_id: ResourceId,
1323 }
1324
1325 impl crate::driver::Installer for TestInstaller {
1326 type Poller = TestHandle;
1327
1328 fn install(self, world: &mut WorldBuilder) -> TestHandle {
1329 let counter_id = world.register::<u64>(0);
1330 TestHandle { counter_id }
1331 }
1332 }
1333
1334 let mut builder = WorldBuilder::new();
1335 let handle = builder.install_driver(TestInstaller);
1336 let world = builder.build();
1337
1338 // Handle's pre-resolved ID can access the resource.
1339 unsafe {
1340 assert_eq!(*world.get::<u64>(handle.counter_id), 0);
1341 }
1342 }
1343
1344 // -- check_access slow path (>128 resources) ------------------------------
1345
1346 #[test]
1347 fn check_access_slow_path_no_conflict() {
1348 // Register 130 distinct types to force the slow path (>128).
1349 macro_rules! register_many {
1350 ($builder:expr, $($i:literal),* $(,)?) => {
1351 $(
1352 $builder.register::<[u8; $i]>([0u8; $i]);
1353 )*
1354 };
1355 }
1356
1357 let mut builder = WorldBuilder::new();
1358 register_many!(
1359 builder, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1360 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
1361 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
1362 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
1363 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
1364 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
1365 125, 126, 127, 128, 129, 130
1366 );
1367 assert!(builder.len() > 128);
1368
1369 // Non-conflicting accesses at high indices — exercises slow path.
1370 let accesses = [(Some(ResourceId(0)), "a"), (Some(ResourceId(129)), "b")];
1371 builder.registry_mut().check_access(&accesses);
1372 }
1373
1374 #[test]
1375 #[should_panic(expected = "conflicting access")]
1376 fn check_access_slow_path_detects_conflict() {
1377 macro_rules! register_many {
1378 ($builder:expr, $($i:literal),* $(,)?) => {
1379 $(
1380 $builder.register::<[u8; $i]>([0u8; $i]);
1381 )*
1382 };
1383 }
1384
1385 let mut builder = WorldBuilder::new();
1386 register_many!(
1387 builder, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1388 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
1389 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
1390 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
1391 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
1392 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
1393 125, 126, 127, 128, 129, 130
1394 );
1395
1396 // Duplicate access at index 129 — must panic.
1397 let accesses = [(Some(ResourceId(129)), "a"), (Some(ResourceId(129)), "b")];
1398 builder.registry_mut().check_access(&accesses);
1399 }
1400
1401 #[test]
1402 fn sequence_wrapping() {
1403 let mut builder = WorldBuilder::new();
1404 builder.register::<u64>(0);
1405 let mut world = builder.build();
1406
1407 // Advance to MAX.
1408 world.current_sequence = Sequence(u64::MAX);
1409 assert_eq!(world.current_sequence(), Sequence(u64::MAX));
1410
1411 // Stamp resource at MAX.
1412 *world.resource_mut::<u64>() = 99;
1413 let id = world.id::<u64>();
1414 unsafe {
1415 assert_eq!(world.changed_at(id), Sequence(u64::MAX));
1416 }
1417
1418 // Wrap to 0.
1419 let seq = world.next_sequence();
1420 assert_eq!(seq, Sequence(0));
1421 assert_eq!(world.current_sequence(), Sequence(0));
1422
1423 // Resource changed at MAX, current is 0 → not changed.
1424 unsafe {
1425 assert_ne!(world.changed_at(id), world.current_sequence());
1426 }
1427 }
1428
1429 // -- BorrowTracker tests (debug builds only) ------------------------------
1430
1431 #[cfg(debug_assertions)]
1432 #[test]
1433 #[should_panic(expected = "conflicting access")]
1434 fn borrow_tracker_catches_double_access() {
1435 let mut builder = WorldBuilder::new();
1436 let id = builder.register::<u64>(42);
1437 let world = builder.build();
1438 world.clear_borrows();
1439 world.track_borrow(id);
1440 world.track_borrow(id); // same resource, same phase
1441 }
1442
1443 #[cfg(debug_assertions)]
1444 #[test]
1445 fn borrow_tracker_allows_after_clear() {
1446 let mut builder = WorldBuilder::new();
1447 let id = builder.register::<u64>(42);
1448 let world = builder.build();
1449 world.clear_borrows();
1450 world.track_borrow(id);
1451 world.clear_borrows();
1452 world.track_borrow(id); // new phase, no conflict
1453 }
1454
1455 #[cfg(debug_assertions)]
1456 #[test]
1457 fn borrow_tracker_different_resources_ok() {
1458 let mut builder = WorldBuilder::new();
1459 let id_a = builder.register::<u64>(1);
1460 let id_b = builder.register::<u32>(2);
1461 let world = builder.build();
1462 world.clear_borrows();
1463 world.track_borrow(id_a);
1464 world.track_borrow(id_b); // different resources, no conflict
1465 }
1466}