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 [`SystemParam::init`] and
10//! [`IntoSystem::into_system`] so that systems 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 systems 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;
31use std::marker::PhantomData;
32
33use rustc_hash::FxHashMap;
34
35// =============================================================================
36// Core types
37// =============================================================================
38
39/// Dense index identifying a resource type within a [`World`] container.
40///
41/// Assigned sequentially at registration (0, 1, 2, ...). Used as a direct
42/// index into internal storage at dispatch time — no hashing, no searching.
43///
44/// Only obtained from [`Registry::id`], [`World::id`], or their `try_` variants.
45#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
46pub struct ResourceId(usize);
47
48/// Monotonic event sequence number for change detection.
49///
50/// Each event processed by a driver is assigned a unique sequence number
51/// via [`World::next_sequence`]. Resources record the sequence at which
52/// they were last written. A resource is considered "changed" if its
53/// `changed_at` equals the world's `current_sequence`. Wrapping is
54/// harmless — only equality is checked.
55#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
56pub struct Sequence(pub(crate) u64);
57
58/// Type-erased drop function. Monomorphized at registration time so we
59/// can reconstruct and drop the original `Box<T>` from a `*mut u8`.
60pub(crate) type DropFn = unsafe fn(*mut u8);
61
62/// Reconstruct and drop a `Box<T>` from a raw pointer.
63///
64/// # Safety
65///
66/// `ptr` must have been produced by `Box::into_raw(Box::new(value))`
67/// where `value: T`. Must only be called once per pointer.
68pub(crate) unsafe fn drop_resource<T>(ptr: *mut u8) {
69 // SAFETY: ptr was produced by Box::into_raw(Box::new(value))
70 // where value: T. Called exactly once in Storage::drop.
71 unsafe {
72 let _ = Box::from_raw(ptr as *mut T);
73 }
74}
75
76// =============================================================================
77// Registry — type-to-index mapping
78// =============================================================================
79
80/// Type-to-index mapping shared between [`WorldBuilder`] and [`World`].
81///
82/// Contains only the type registry — no storage, no pointers. Passed to
83/// [`IntoSystem::into_system`](crate::IntoSystem::into_system) and
84/// [`SystemParam::init`](crate::SystemParam::init) so systems can resolve
85/// [`ResourceId`]s during driver setup.
86///
87/// Obtained via [`WorldBuilder::registry()`] or [`World::registry()`].
88pub struct Registry {
89 indices: FxHashMap<TypeId, ResourceId>,
90 /// Scratch bitset reused across [`check_access`](Self::check_access) calls.
91 /// Allocated once on the first call with >64 resources, then reused.
92 scratch: Vec<u64>,
93}
94
95impl Registry {
96 pub(crate) fn new() -> Self {
97 Self {
98 indices: FxHashMap::default(),
99 scratch: Vec::new(),
100 }
101 }
102
103 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
104 ///
105 /// # Panics
106 ///
107 /// Panics if the resource type was not registered.
108 pub fn id<T: 'static>(&self) -> ResourceId {
109 *self
110 .indices
111 .get(&TypeId::of::<T>())
112 .unwrap_or_else(|| panic!("resource `{}` not registered", type_name::<T>()))
113 }
114
115 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
116 /// type was not registered.
117 pub fn try_id<T: 'static>(&self) -> Option<ResourceId> {
118 self.indices.get(&TypeId::of::<T>()).copied()
119 }
120
121 /// Returns `true` if a resource of type `T` has been registered.
122 pub fn contains<T: 'static>(&self) -> bool {
123 self.indices.contains_key(&TypeId::of::<T>())
124 }
125
126 /// Returns the number of registered resources.
127 pub fn len(&self) -> usize {
128 self.indices.len()
129 }
130
131 /// Returns `true` if no resources have been registered.
132 pub fn is_empty(&self) -> bool {
133 self.indices.is_empty()
134 }
135
136 /// Validate that a set of parameter accesses don't conflict.
137 ///
138 /// Two accesses conflict when they target the same ResourceId.
139 /// Called at construction time by `into_system`, `into_callback`,
140 /// and `into_stage`.
141 ///
142 /// Fast path (≤128 resources): single `u128` on the stack, zero heap.
143 /// Slow path (>128 resources): reusable `Vec<u64>` owned by Registry —
144 /// allocated once on first use, then cleared and reused.
145 ///
146 /// # Panics
147 ///
148 /// Panics if any resource is accessed by more than one parameter.
149 #[cold]
150 pub fn check_access(&mut self, accesses: &[(Option<ResourceId>, &str)]) {
151 let n = self.len();
152 if n == 0 {
153 return;
154 }
155
156 if n <= 128 {
157 // Fast path: single u128 on the stack.
158 let mut seen = 0u128;
159 for &(id, name) in accesses {
160 let Some(id) = id else { continue };
161 let bit = 1u128 << id.0;
162 assert!(
163 seen & bit == 0,
164 "conflicting access: resource borrowed by `{}` is already \
165 borrowed by another parameter in the same system",
166 name,
167 );
168 seen |= bit;
169 }
170 } else {
171 // Slow path: reusable heap buffer.
172 let words = n.div_ceil(64);
173 self.scratch.resize(words, 0);
174 self.scratch.fill(0);
175 for &(id, name) in accesses {
176 let Some(id) = id else { continue };
177 let word = id.0 / 64;
178 let bit = 1u64 << (id.0 % 64);
179 assert!(
180 self.scratch[word] & bit == 0,
181 "conflicting access: resource borrowed by `{}` is already \
182 borrowed by another parameter in the same system",
183 name,
184 );
185 self.scratch[word] |= bit;
186 }
187 }
188 }
189}
190
191// =============================================================================
192// Storage — shared backing between builder and frozen container
193// =============================================================================
194
195/// Interleaved pointer + change sequence for a single resource.
196/// 16 bytes — 4 slots per cache line.
197#[repr(C)]
198pub(crate) struct ResourceSlot {
199 pub(crate) ptr: *mut u8,
200 pub(crate) changed_at: Cell<Sequence>,
201}
202
203/// Internal storage for type-erased resource pointers and their destructors.
204///
205/// Owns the heap allocations and is responsible for cleanup. Shared between
206/// [`WorldBuilder`] and [`World`] via move — avoids duplicating Drop logic.
207pub(crate) struct Storage {
208 /// Dense array of interleaved pointer + change sequence pairs.
209 /// Each pointer was produced by `Box::into_raw`.
210 pub(crate) slots: Vec<ResourceSlot>,
211 /// Parallel array of drop functions. `drop_fns[i]` is the monomorphized
212 /// destructor for the concrete type behind `slots[i].ptr`.
213 pub(crate) drop_fns: Vec<DropFn>,
214}
215
216impl Storage {
217 pub(crate) fn new() -> Self {
218 Self {
219 slots: Vec::new(),
220 drop_fns: Vec::new(),
221 }
222 }
223
224 pub(crate) fn len(&self) -> usize {
225 self.slots.len()
226 }
227
228 pub(crate) fn is_empty(&self) -> bool {
229 self.slots.is_empty()
230 }
231}
232
233// SAFETY: All values stored in Storage were registered via `register<T: Send + 'static>`,
234// so every concrete type behind the raw pointers is Send. Storage exclusively owns
235// these heap allocations — they are not aliased or shared. Transferring ownership
236// to another thread is safe. Cell<Sequence> is !Sync but we're transferring
237// ownership, not sharing.
238#[allow(clippy::non_send_fields_in_send_ty)]
239unsafe impl Send for Storage {}
240
241impl Drop for Storage {
242 fn drop(&mut self) {
243 for (slot, drop_fn) in self.slots.iter().zip(&self.drop_fns) {
244 // SAFETY: each (slot.ptr, drop_fn) pair was created together in
245 // WorldBuilder::register(). drop_fn is the monomorphized
246 // destructor for the concrete type behind ptr. Called exactly
247 // once here.
248 unsafe {
249 drop_fn(slot.ptr);
250 }
251 }
252 }
253}
254
255// =============================================================================
256// WorldBuilder
257// =============================================================================
258
259/// Builder for registering resources before freezing into a [`World`] container.
260///
261/// Each resource type can only be registered once. Registration assigns a
262/// dense [`ResourceId`] index (0, 1, 2, ...).
263///
264/// The [`registry()`](Self::registry) method exposes the type-to-index mapping
265/// so that drivers can resolve systems against the builder before `build()`.
266///
267/// # Examples
268///
269/// ```
270/// use nexus_rt::WorldBuilder;
271///
272/// let mut builder = WorldBuilder::new();
273/// builder.register::<u64>(42);
274/// builder.register::<bool>(true);
275/// let world = builder.build();
276///
277/// let id = world.id::<u64>();
278/// unsafe {
279/// assert_eq!(*world.get::<u64>(id), 42);
280/// }
281/// ```
282pub struct WorldBuilder {
283 registry: Registry,
284 storage: Storage,
285}
286
287impl WorldBuilder {
288 /// Create an empty builder.
289 pub fn new() -> Self {
290 Self {
291 registry: Registry::new(),
292 storage: Storage::new(),
293 }
294 }
295
296 /// Register a resource.
297 ///
298 /// The value is heap-allocated via `Box` and ownership is transferred
299 /// to the container. The pointer is stable for the lifetime of the
300 /// resulting [`World`].
301 ///
302 /// # Panics
303 ///
304 /// Panics if a resource of the same type is already registered.
305 #[cold]
306 pub fn register<T: Send + 'static>(&mut self, value: T) -> &mut Self {
307 let type_id = TypeId::of::<T>();
308 assert!(
309 !self.registry.indices.contains_key(&type_id),
310 "resource `{}` already registered",
311 type_name::<T>(),
312 );
313
314 let ptr = Box::into_raw(Box::new(value)) as *mut u8;
315 let id = ResourceId(self.storage.slots.len());
316 self.registry.indices.insert(type_id, id);
317 self.storage.slots.push(ResourceSlot {
318 ptr,
319 changed_at: Cell::new(Sequence(0)),
320 });
321 self.storage.drop_fns.push(drop_resource::<T>);
322 self
323 }
324
325 /// Register a resource using its [`Default`] value.
326 ///
327 /// Equivalent to `self.register::<T>(T::default())`.
328 #[cold]
329 pub fn register_default<T: Default + Send + 'static>(&mut self) -> &mut Self {
330 self.register(T::default())
331 }
332
333 /// Returns a shared reference to the type registry.
334 ///
335 /// Use this for read-only queries. For construction-time calls
336 /// like [`into_system`](crate::IntoSystem::into_system), use
337 /// [`registry_mut`](Self::registry_mut) instead.
338 pub fn registry(&self) -> &Registry {
339 &self.registry
340 }
341
342 /// Returns a mutable reference to the type registry.
343 ///
344 /// Needed at construction time for
345 /// [`into_system`](crate::IntoSystem::into_system),
346 /// [`into_callback`](crate::IntoCallback::into_callback), and
347 /// [`into_stage`](crate::IntoStage::into_stage)
348 /// which call [`Registry::check_access`].
349 pub fn registry_mut(&mut self) -> &mut Registry {
350 &mut self.registry
351 }
352
353 /// Returns the number of registered resources.
354 pub fn len(&self) -> usize {
355 self.storage.len()
356 }
357
358 /// Returns `true` if no resources have been registered.
359 pub fn is_empty(&self) -> bool {
360 self.storage.is_empty()
361 }
362
363 /// Returns `true` if a resource of type `T` has been registered.
364 pub fn contains<T: 'static>(&self) -> bool {
365 self.registry.contains::<T>()
366 }
367
368 /// Install a plugin. The plugin is consumed and registers its
369 /// resources into this builder.
370 pub fn install_plugin(&mut self, plugin: impl crate::plugin::Plugin) -> &mut Self {
371 plugin.build(self);
372 self
373 }
374
375 /// Install a driver. The driver is consumed, registers its resources
376 /// into this builder, and returns a concrete handle for dispatch-time
377 /// polling.
378 pub fn install_driver<D: crate::driver::Driver>(&mut self, driver: D) -> D::Handle {
379 driver.install(self)
380 }
381
382 /// Freeze the builder into an immutable [`World`] container.
383 ///
384 /// After this call, no more resources can be registered. All
385 /// [`ResourceId`] values remain valid for the lifetime of the
386 /// returned [`World`].
387 pub fn build(self) -> World {
388 World {
389 registry: self.registry,
390 storage: self.storage,
391 current_sequence: Sequence(0),
392 _not_sync: PhantomData,
393 }
394 }
395}
396
397impl Default for WorldBuilder {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403// =============================================================================
404// World — frozen container
405// =============================================================================
406
407/// Frozen singleton resource storage.
408///
409/// Created by [`WorldBuilder::build()`]. Resources are indexed by dense
410/// [`ResourceId`] for O(1) dispatch-time access (~3 cycles per fetch).
411///
412/// # Safe API
413///
414/// - [`resource`](Self::resource) / [`resource_mut`](Self::resource_mut) —
415/// cold-path access via HashMap lookup.
416///
417/// # Unsafe API (framework internals)
418///
419/// The low-level `get` / `get_mut` methods are `unsafe` — used by
420/// [`SystemParam::fetch`](crate::SystemParam) for ~3-cycle dispatch.
421/// The caller must ensure no mutable aliasing.
422pub struct World {
423 /// Type-to-index mapping. Same registry used during build.
424 registry: Registry,
425 /// Type-erased pointer storage. Drop handled by `Storage`.
426 storage: Storage,
427 /// Current sequence number. Advanced by the driver before
428 /// each event dispatch.
429 current_sequence: Sequence,
430 /// World must not be shared across threads — it holds interior-mutable
431 /// `Cell<Sequence>` values accessed through `&self`. `!Sync` enforced by
432 /// `PhantomData<Cell<()>>`.
433 _not_sync: PhantomData<Cell<()>>,
434}
435
436impl World {
437 /// Convenience constructor — returns a new [`WorldBuilder`].
438 pub fn builder() -> WorldBuilder {
439 WorldBuilder::new()
440 }
441
442 /// Returns a shared reference to the type registry.
443 ///
444 /// Use this for read-only queries (e.g. [`id`](Registry::id),
445 /// [`contains`](Registry::contains)). For construction-time calls
446 /// like [`into_system`](crate::IntoSystem::into_system), use
447 /// [`registry_mut`](Self::registry_mut) instead.
448 pub fn registry(&self) -> &Registry {
449 &self.registry
450 }
451
452 /// Returns a mutable reference to the type registry.
453 ///
454 /// Needed at construction time for
455 /// [`into_system`](crate::IntoSystem::into_system),
456 /// [`into_callback`](crate::IntoCallback::into_callback), and
457 /// [`into_stage`](crate::IntoStage::into_stage)
458 /// which call [`Registry::check_access`].
459 pub fn registry_mut(&mut self) -> &mut Registry {
460 &mut self.registry
461 }
462
463 /// Resolve the [`ResourceId`] for a type. Cold path — uses HashMap lookup.
464 ///
465 /// # Panics
466 ///
467 /// Panics if the resource type was not registered.
468 pub fn id<T: 'static>(&self) -> ResourceId {
469 self.registry.id::<T>()
470 }
471
472 /// Try to resolve the [`ResourceId`] for a type. Returns `None` if the
473 /// type was not registered.
474 pub fn try_id<T: 'static>(&self) -> Option<ResourceId> {
475 self.registry.try_id::<T>()
476 }
477
478 /// Returns the number of registered resources.
479 pub fn len(&self) -> usize {
480 self.storage.len()
481 }
482
483 /// Returns `true` if no resources are stored.
484 pub fn is_empty(&self) -> bool {
485 self.storage.is_empty()
486 }
487
488 /// Returns `true` if a resource of type `T` is stored.
489 pub fn contains<T: 'static>(&self) -> bool {
490 self.registry.contains::<T>()
491 }
492
493 // =========================================================================
494 // Safe resource access (cold path — HashMap lookup per call)
495 // =========================================================================
496
497 /// Safe shared access to a resource. Cold path — resolves via HashMap.
498 ///
499 /// Takes `&self` — multiple shared references can coexist. The borrow
500 /// checker prevents mixing with [`resource_mut`](Self::resource_mut)
501 /// (which takes `&mut self`).
502 ///
503 /// # Panics
504 ///
505 /// Panics if the resource type was not registered.
506 pub fn resource<T: 'static>(&self) -> &T {
507 let id = self.registry.id::<T>();
508 // SAFETY: id resolved from our own registry. &self prevents mutable
509 // aliases — resource_mut takes &mut self.
510 unsafe { self.get(id) }
511 }
512
513 /// Safe exclusive access to a resource. Cold path — resolves via HashMap.
514 ///
515 /// # Panics
516 ///
517 /// Panics if the resource type was not registered.
518 pub fn resource_mut<T: 'static>(&mut self) -> &mut T {
519 let id = self.registry.id::<T>();
520 // Cold path — stamp unconditionally. If you request &mut, you're writing.
521 self.storage.slots[id.0]
522 .changed_at
523 .set(self.current_sequence);
524 // SAFETY: id resolved from our own registry. &mut self ensures
525 // exclusive access — no other references can exist.
526 unsafe { self.get_mut(id) }
527 }
528
529 // =========================================================================
530 // Sequence / change detection
531 // =========================================================================
532
533 /// Returns the current event sequence number.
534 pub fn current_sequence(&self) -> Sequence {
535 self.current_sequence
536 }
537
538 /// Advance to the next event sequence number and return it.
539 ///
540 /// Drivers call this before dispatching each event. The returned
541 /// sequence number identifies the event being processed. Resources
542 /// mutated during dispatch will record this sequence in `changed_at`.
543 pub fn next_sequence(&mut self) -> Sequence {
544 self.current_sequence = Sequence(self.current_sequence.0.wrapping_add(1));
545 self.current_sequence
546 }
547
548 // =========================================================================
549 // Unsafe resource access (hot path — pre-resolved ResourceId)
550 // =========================================================================
551
552 /// Fetch a shared reference to a resource by pre-validated index.
553 ///
554 /// # Safety
555 ///
556 /// - `id` must have been returned by [`WorldBuilder::register`] for
557 /// the same builder that produced this container.
558 /// - `T` must be the same type that was registered at this `id`.
559 /// - The caller must ensure no mutable reference to this resource exists.
560 #[inline(always)]
561 pub unsafe fn get<T: 'static>(&self, id: ResourceId) -> &T {
562 // SAFETY: caller guarantees id was returned by register() on the
563 // builder that produced this container, so id.0 < self.storage.slots.len().
564 // T matches the registered type. No mutable alias exists.
565 unsafe { &*(self.get_ptr(id) as *const T) }
566 }
567
568 /// Fetch a mutable reference to a resource by pre-validated index.
569 ///
570 /// Takes `&self` — the container structure is frozen, but individual
571 /// resources have interior mutability via raw pointers. Sound because
572 /// callers (single-threaded sequential dispatch) uphold no-aliasing.
573 ///
574 /// # Safety
575 ///
576 /// - `id` must have been returned by [`WorldBuilder::register`] for
577 /// the same builder that produced this container.
578 /// - `T` must be the same type that was registered at this `id`.
579 /// - The caller must ensure no other reference (shared or mutable) to this
580 /// resource exists.
581 #[inline(always)]
582 #[allow(clippy::mut_from_ref)]
583 pub unsafe fn get_mut<T: 'static>(&self, id: ResourceId) -> &mut T {
584 // SAFETY: caller guarantees id was returned by register() on the
585 // builder that produced this container, so id.0 < self.storage.slots.len().
586 // T matches the registered type. No aliases exist.
587 unsafe { &mut *(self.get_ptr(id) as *mut T) }
588 }
589
590 /// Fetch a raw pointer to a resource by pre-validated index.
591 ///
592 /// Intended for macro-generated dispatch code that needs direct pointer
593 /// access.
594 ///
595 /// # Safety
596 ///
597 /// - `id` must have been returned by [`WorldBuilder::register`] for
598 /// the same builder that produced this container.
599 #[inline(always)]
600 pub unsafe fn get_ptr(&self, id: ResourceId) -> *mut u8 {
601 debug_assert!(
602 id.0 < self.storage.slots.len(),
603 "ResourceId({}) out of bounds (len {})",
604 id.0,
605 self.storage.slots.len(),
606 );
607 // SAFETY: caller guarantees id was returned by register() on the
608 // builder that produced this container, so id.0 < self.storage.slots.len().
609 unsafe { self.storage.slots.get_unchecked(id.0).ptr }
610 }
611
612 // =========================================================================
613 // Change-detection internals (framework use only)
614 // =========================================================================
615
616 /// Read the sequence at which a resource was last changed.
617 ///
618 /// # Safety
619 ///
620 /// `id` must have been returned by [`WorldBuilder::register`] for
621 /// the same builder that produced this container.
622 #[inline(always)]
623 pub(crate) unsafe fn changed_at(&self, id: ResourceId) -> Sequence {
624 unsafe { self.storage.slots.get_unchecked(id.0).changed_at.get() }
625 }
626
627 /// Get a reference to the `Cell` tracking a resource's change sequence.
628 ///
629 /// # Safety
630 ///
631 /// `id` must have been returned by [`WorldBuilder::register`] for
632 /// the same builder that produced this container.
633 #[inline(always)]
634 pub(crate) unsafe fn changed_at_cell(&self, id: ResourceId) -> &Cell<Sequence> {
635 unsafe { &self.storage.slots.get_unchecked(id.0).changed_at }
636 }
637
638 /// Stamp a resource as changed at the current sequence.
639 ///
640 /// # Safety
641 ///
642 /// `id` must have been returned by [`WorldBuilder::register`] for
643 /// the same builder that produced this container.
644 #[inline(always)]
645 #[allow(dead_code)] // Available for driver implementations.
646 pub(crate) unsafe fn stamp_changed(&self, id: ResourceId) {
647 unsafe {
648 self.storage
649 .slots
650 .get_unchecked(id.0)
651 .changed_at
652 .set(self.current_sequence);
653 }
654 }
655}
656
657// SAFETY: All resources are `T: Send` (enforced by `register`). World owns all
658// heap-allocated data exclusively — the raw pointers are not aliased or shared.
659// Transferring ownership to another thread is safe; the new thread becomes the
660// sole accessor.
661unsafe impl Send for World {}
662
663// =============================================================================
664// Tests
665// =============================================================================
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use std::sync::{Arc, Weak};
671
672 struct Price {
673 value: f64,
674 }
675
676 struct Venue {
677 name: &'static str,
678 }
679
680 struct Config {
681 max_orders: usize,
682 }
683
684 #[test]
685 fn register_and_build() {
686 let mut builder = WorldBuilder::new();
687 builder
688 .register::<Price>(Price { value: 100.0 })
689 .register::<Venue>(Venue { name: "test" });
690 let world = builder.build();
691 assert_eq!(world.len(), 2);
692 }
693
694 #[test]
695 fn resource_ids_are_sequential() {
696 let mut builder = WorldBuilder::new();
697 builder
698 .register::<Price>(Price { value: 0.0 })
699 .register::<Venue>(Venue { name: "" })
700 .register::<Config>(Config { max_orders: 0 });
701 let world = builder.build();
702 assert_eq!(world.id::<Price>(), ResourceId(0));
703 assert_eq!(world.id::<Venue>(), ResourceId(1));
704 assert_eq!(world.id::<Config>(), ResourceId(2));
705 }
706
707 #[test]
708 fn get_returns_registered_value() {
709 let mut builder = WorldBuilder::new();
710 builder.register::<Price>(Price { value: 42.5 });
711 let world = builder.build();
712
713 let id = world.id::<Price>();
714 // SAFETY: id resolved from this container, type matches, no aliasing.
715 let price = unsafe { world.get::<Price>(id) };
716 assert_eq!(price.value, 42.5);
717 }
718
719 #[test]
720 fn get_mut_modifies_value() {
721 let mut builder = WorldBuilder::new();
722 builder.register::<Price>(Price { value: 1.0 });
723 let world = builder.build();
724
725 let id = world.id::<Price>();
726 // SAFETY: id resolved from this container, type matches, no aliasing.
727 unsafe {
728 world.get_mut::<Price>(id).value = 99.0;
729 assert_eq!(world.get::<Price>(id).value, 99.0);
730 }
731 }
732
733 #[test]
734 fn try_id_returns_none_for_unregistered() {
735 let world = WorldBuilder::new().build();
736 assert!(world.try_id::<Price>().is_none());
737 }
738
739 #[test]
740 fn try_id_returns_some_for_registered() {
741 let mut builder = WorldBuilder::new();
742 builder.register::<Price>(Price { value: 0.0 });
743 let world = builder.build();
744
745 assert!(world.try_id::<Price>().is_some());
746 }
747
748 #[test]
749 #[should_panic(expected = "already registered")]
750 fn panics_on_duplicate_registration() {
751 let mut builder = WorldBuilder::new();
752 builder.register::<Price>(Price { value: 1.0 });
753 builder.register::<Price>(Price { value: 2.0 });
754 }
755
756 #[test]
757 #[should_panic(expected = "not registered")]
758 fn panics_on_unregistered_id() {
759 let world = WorldBuilder::new().build();
760 world.id::<Price>();
761 }
762
763 #[test]
764 fn empty_builder_builds_empty_world() {
765 let world = WorldBuilder::new().build();
766 assert_eq!(world.len(), 0);
767 assert!(world.is_empty());
768 }
769
770 #[test]
771 fn drop_runs_destructors() {
772 let arc = Arc::new(42u32);
773 let weak: Weak<u32> = Arc::downgrade(&arc);
774
775 {
776 let mut builder = WorldBuilder::new();
777 builder.register::<Arc<u32>>(arc);
778 let _world = builder.build();
779 // Arc still alive — held by World
780 assert!(weak.upgrade().is_some());
781 }
782 // World dropped — Arc should be deallocated
783 assert!(weak.upgrade().is_none());
784 }
785
786 #[test]
787 fn builder_drop_cleans_up_without_build() {
788 let arc = Arc::new(99u32);
789 let weak: Weak<u32> = Arc::downgrade(&arc);
790
791 {
792 let mut builder = WorldBuilder::new();
793 builder.register::<Arc<u32>>(arc);
794 }
795 // Builder dropped without build() — Storage::drop cleans up
796 assert!(weak.upgrade().is_none());
797 }
798
799 #[test]
800 fn multiple_types_independent() {
801 let mut builder = WorldBuilder::new();
802 builder
803 .register::<Price>(Price { value: 10.0 })
804 .register::<Venue>(Venue { name: "CB" })
805 .register::<Config>(Config { max_orders: 500 });
806 let world = builder.build();
807
808 unsafe {
809 let price_id = world.id::<Price>();
810 let venue_id = world.id::<Venue>();
811 let config_id = world.id::<Config>();
812 assert_eq!(world.get::<Price>(price_id).value, 10.0);
813 assert_eq!(world.get::<Venue>(venue_id).name, "CB");
814 assert_eq!(world.get::<Config>(config_id).max_orders, 500);
815 }
816 }
817
818 #[test]
819 fn contains_reflects_registration() {
820 let mut builder = WorldBuilder::new();
821 assert!(!builder.contains::<Price>());
822
823 builder.register::<Price>(Price { value: 0.0 });
824 assert!(builder.contains::<Price>());
825 assert!(!builder.contains::<Venue>());
826
827 let world = builder.build();
828 assert!(world.contains::<Price>());
829 assert!(!world.contains::<Venue>());
830 }
831
832 #[test]
833 fn get_ptr_returns_valid_pointer() {
834 let mut builder = WorldBuilder::new();
835 builder.register::<Price>(Price { value: 77.7 });
836 let world = builder.build();
837
838 let id = world.id::<Price>();
839 unsafe {
840 let ptr = world.get_ptr(id);
841 let price = &*(ptr as *const Price);
842 assert_eq!(price.value, 77.7);
843 }
844 }
845
846 #[test]
847 fn send_to_another_thread() {
848 let mut builder = WorldBuilder::new();
849 builder.register::<Price>(Price { value: 55.5 });
850 let world = builder.build();
851
852 let handle = std::thread::spawn(move || {
853 let id = world.id::<Price>();
854 // SAFETY: sole owner on this thread, no aliasing.
855 unsafe { world.get::<Price>(id).value }
856 });
857 assert_eq!(handle.join().unwrap(), 55.5);
858 }
859
860 #[test]
861 fn registry_accessible_from_builder() {
862 let mut builder = WorldBuilder::new();
863 builder.register::<u64>(42);
864
865 let registry = builder.registry();
866 assert!(registry.contains::<u64>());
867 assert!(!registry.contains::<bool>());
868
869 let id = registry.id::<u64>();
870 assert_eq!(id, ResourceId(0));
871 }
872
873 #[test]
874 fn registry_accessible_from_world() {
875 let mut builder = WorldBuilder::new();
876 builder.register::<u64>(42);
877 let world = builder.build();
878
879 let registry = world.registry();
880 assert!(registry.contains::<u64>());
881
882 // Registry from world and world.id() agree.
883 assert_eq!(registry.id::<u64>(), world.id::<u64>());
884 }
885
886 // -- Safe accessor tests --------------------------------------------------
887
888 #[test]
889 fn resource_reads_value() {
890 let mut builder = WorldBuilder::new();
891 builder.register::<Price>(Price { value: 42.5 });
892 let world = builder.build();
893
894 assert_eq!(world.resource::<Price>().value, 42.5);
895 }
896
897 #[test]
898 fn resource_mut_modifies_value() {
899 let mut builder = WorldBuilder::new();
900 builder.register::<u64>(0);
901 let mut world = builder.build();
902
903 *world.resource_mut::<u64>() = 99;
904 assert_eq!(*world.resource::<u64>(), 99);
905 }
906
907 #[test]
908 fn register_default_works() {
909 let mut builder = WorldBuilder::new();
910 builder.register_default::<Vec<u32>>();
911 let world = builder.build();
912
913 let v = world.resource::<Vec<u32>>();
914 assert!(v.is_empty());
915 }
916
917 // -- Sequence / change detection tests ----------------------------------------
918
919 #[test]
920 fn sequence_default_is_zero() {
921 assert_eq!(Sequence::default(), Sequence(0));
922 }
923
924 #[test]
925 fn next_sequence_increments() {
926 let mut world = WorldBuilder::new().build();
927 assert_eq!(world.current_sequence(), Sequence(0));
928 world.next_sequence();
929 assert_eq!(world.current_sequence(), Sequence(1));
930 world.next_sequence();
931 assert_eq!(world.current_sequence(), Sequence(2));
932 }
933
934 #[test]
935 fn resource_registered_at_current_sequence() {
936 // Resources registered at build time get changed_at=Sequence(0).
937 // World starts at current_sequence=Sequence(0). So they match — "changed."
938 let mut builder = WorldBuilder::new();
939 builder.register::<u64>(42);
940 let world = builder.build();
941
942 let id = world.id::<u64>();
943 unsafe {
944 assert_eq!(world.changed_at(id), Sequence(0));
945 assert_eq!(world.current_sequence(), Sequence(0));
946 // changed_at == current_sequence → "changed"
947 assert_eq!(world.changed_at(id), world.current_sequence());
948 }
949 }
950
951 #[test]
952 fn resource_mut_stamps_changed_at() {
953 let mut builder = WorldBuilder::new();
954 builder.register::<u64>(0);
955 let mut world = builder.build();
956
957 world.next_sequence(); // tick=1
958 let id = world.id::<u64>();
959
960 // changed_at is still 0, current_sequence is 1 → not changed
961 unsafe {
962 assert_eq!(world.changed_at(id), Sequence(0));
963 }
964
965 // resource_mut stamps changed_at to current_sequence
966 *world.resource_mut::<u64>() = 99;
967 unsafe {
968 assert_eq!(world.changed_at(id), Sequence(1));
969 }
970 }
971
972 // -- Plugin / Driver tests ------------------------------------------------
973
974 #[test]
975 fn plugin_registers_resources() {
976 struct TestPlugin;
977
978 impl crate::plugin::Plugin for TestPlugin {
979 fn build(self, world: &mut WorldBuilder) {
980 world.register::<u64>(42);
981 world.register::<bool>(true);
982 }
983 }
984
985 let mut builder = WorldBuilder::new();
986 builder.install_plugin(TestPlugin);
987 let world = builder.build();
988
989 assert_eq!(*world.resource::<u64>(), 42);
990 assert_eq!(*world.resource::<bool>(), true);
991 }
992
993 #[test]
994 fn driver_installs_and_returns_handle() {
995 struct TestInstaller;
996 struct TestHandle {
997 counter_id: ResourceId,
998 }
999
1000 impl crate::driver::Driver for TestInstaller {
1001 type Handle = TestHandle;
1002
1003 fn install(self, world: &mut WorldBuilder) -> TestHandle {
1004 world.register::<u64>(0);
1005 let counter_id = world.registry().id::<u64>();
1006 TestHandle { counter_id }
1007 }
1008 }
1009
1010 let mut builder = WorldBuilder::new();
1011 let handle = builder.install_driver(TestInstaller);
1012 let world = builder.build();
1013
1014 // Handle's pre-resolved ID can access the resource.
1015 unsafe {
1016 assert_eq!(*world.get::<u64>(handle.counter_id), 0);
1017 }
1018 }
1019
1020 // -- check_access slow path (>128 resources) ------------------------------
1021
1022 #[test]
1023 fn check_access_slow_path_no_conflict() {
1024 // Register 130 distinct types to force the slow path (>128).
1025 macro_rules! register_many {
1026 ($builder:expr, $($i:literal),* $(,)?) => {
1027 $(
1028 $builder.register::<[u8; $i]>([0u8; $i]);
1029 )*
1030 };
1031 }
1032
1033 let mut builder = WorldBuilder::new();
1034 register_many!(
1035 builder, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1036 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
1037 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
1038 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
1039 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
1040 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
1041 125, 126, 127, 128, 129, 130
1042 );
1043 assert!(builder.len() > 128);
1044
1045 // Non-conflicting accesses at high indices — exercises slow path.
1046 let accesses = [(Some(ResourceId(0)), "a"), (Some(ResourceId(129)), "b")];
1047 builder.registry_mut().check_access(&accesses);
1048 }
1049
1050 #[test]
1051 #[should_panic(expected = "conflicting access")]
1052 fn check_access_slow_path_detects_conflict() {
1053 macro_rules! register_many {
1054 ($builder:expr, $($i:literal),* $(,)?) => {
1055 $(
1056 $builder.register::<[u8; $i]>([0u8; $i]);
1057 )*
1058 };
1059 }
1060
1061 let mut builder = WorldBuilder::new();
1062 register_many!(
1063 builder, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
1064 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
1065 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
1066 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
1067 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
1068 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
1069 125, 126, 127, 128, 129, 130
1070 );
1071
1072 // Duplicate access at index 129 — must panic.
1073 let accesses = [(Some(ResourceId(129)), "a"), (Some(ResourceId(129)), "b")];
1074 builder.registry_mut().check_access(&accesses);
1075 }
1076
1077 #[test]
1078 fn sequence_wrapping() {
1079 let mut builder = WorldBuilder::new();
1080 builder.register::<u64>(0);
1081 let mut world = builder.build();
1082
1083 // Advance to MAX.
1084 world.current_sequence = Sequence(u64::MAX);
1085 assert_eq!(world.current_sequence(), Sequence(u64::MAX));
1086
1087 // Stamp resource at MAX.
1088 *world.resource_mut::<u64>() = 99;
1089 let id = world.id::<u64>();
1090 unsafe {
1091 assert_eq!(world.changed_at(id), Sequence(u64::MAX));
1092 }
1093
1094 // Wrap to 0.
1095 let seq = world.next_sequence();
1096 assert_eq!(seq, Sequence(0));
1097 assert_eq!(world.current_sequence(), Sequence(0));
1098
1099 // Resource changed at MAX, current is 0 → not changed.
1100 unsafe {
1101 assert_ne!(world.changed_at(id), world.current_sequence());
1102 }
1103 }
1104}