Skip to main content

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}