Skip to main content

arkhe_kernel/abi/
ids.rs

1//! Identifier newtypes.
2//!
3//! `InstanceId` and `EntityId` wrap `NonZeroU64` — zero is structurally
4//! unrepresentable, eliminating sentinel-value traps. `Tick`, `TypeCode`,
5//! `RouteId` wrap plain primitives because zero is a meaningful value for
6//! all three (origin tick; type/route zero is reserved for "unassigned"
7//! debug placeholders only and is not a sentinel).
8
9use core::num::NonZeroU64;
10use serde::{Deserialize, Serialize};
11
12/// Instance namespace handle. Non-zero to reject sentinel usage at the
13/// type level.
14#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct InstanceId(NonZeroU64);
17
18impl InstanceId {
19    /// Returns `Some(_)` iff `v != 0`.
20    #[inline]
21    pub const fn new(v: u64) -> Option<Self> {
22        match NonZeroU64::new(v) {
23            Some(n) => Some(Self(n)),
24            None => None,
25        }
26    }
27
28    /// Underlying non-zero `u64`.
29    #[inline]
30    pub const fn get(self) -> u64 {
31        self.0.get()
32    }
33}
34
35/// Per-instance entity handle. Non-zero.
36#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
37#[serde(transparent)]
38pub struct EntityId(NonZeroU64);
39
40impl EntityId {
41    /// Returns `Some(_)` iff `v != 0`.
42    #[inline]
43    pub const fn new(v: u64) -> Option<Self> {
44        match NonZeroU64::new(v) {
45            Some(n) => Some(Self(n)),
46            None => None,
47        }
48    }
49
50    /// Underlying non-zero `u64`.
51    #[inline]
52    pub const fn get(self) -> u64 {
53        self.0.get()
54    }
55}
56
57/// Deterministic logical time. Monotonic non-decreasing across `step()`
58/// boundaries of an instance. Zero is the origin tick.
59#[derive(
60    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize,
61)]
62#[serde(transparent)]
63pub struct Tick(pub u64);
64
65impl Tick {
66    /// Origin tick — fresh kernels and fresh instances start here.
67    pub const ZERO: Tick = Tick(0);
68
69    /// Advance by `delta` using saturating addition (no wraparound).
70    #[inline]
71    pub const fn advance(self, delta: u64) -> Tick {
72        Tick(self.0.saturating_add(delta))
73    }
74}
75
76/// Stable dispatch identifier for a registered Action/Component/Event type.
77/// Assigned monotonically at `register_module` and bound to a schema_hash for
78/// the lifetime of the world (TypeCode cross-restart persistence).
79#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
80#[serde(transparent)]
81pub struct TypeCode(pub u32);
82
83/// Stable dispatch identifier for an action route. Registry interns string
84/// names into `RouteId` at registration; kernel internal dispatch uses
85/// `RouteId` only ("string-free dispatch").
86#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
87#[serde(transparent)]
88pub struct RouteId(pub u32);
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn instance_id_zero_is_unrepresentable() {
96        assert!(InstanceId::new(0).is_none());
97    }
98
99    #[test]
100    fn instance_id_nonzero_roundtrip() {
101        let id = InstanceId::new(42).expect("42 is non-zero");
102        assert_eq!(id.get(), 42);
103    }
104
105    #[test]
106    fn entity_id_zero_is_unrepresentable() {
107        assert!(EntityId::new(0).is_none());
108    }
109
110    #[test]
111    fn entity_id_nonzero_roundtrip() {
112        let id = EntityId::new(u64::MAX).expect("max is non-zero");
113        assert_eq!(id.get(), u64::MAX);
114    }
115
116    #[test]
117    fn tick_advance_saturates_without_wrapping() {
118        assert_eq!(Tick::ZERO.advance(100).0, 100);
119        assert_eq!(Tick(u64::MAX).advance(1).0, u64::MAX);
120        assert_eq!(Tick(u64::MAX - 5).advance(10).0, u64::MAX);
121    }
122
123    #[test]
124    fn type_code_and_route_id_are_totally_ordered() {
125        assert!(TypeCode(1) < TypeCode(2));
126        assert!(RouteId(10) > RouteId(5));
127    }
128
129    #[test]
130    fn ids_are_copy_and_eq() {
131        // Compile-time proof via trait bounds.
132        fn assert_copy<T: Copy>() {}
133        fn assert_eq<T: Eq>() {}
134        fn assert_ord<T: Ord>() {}
135        fn assert_hash<T: core::hash::Hash>() {}
136        assert_copy::<InstanceId>();
137        assert_copy::<EntityId>();
138        assert_copy::<Tick>();
139        assert_copy::<TypeCode>();
140        assert_copy::<RouteId>();
141        assert_eq::<InstanceId>();
142        assert_ord::<InstanceId>();
143        assert_hash::<InstanceId>();
144    }
145}