Skip to main content

moirai/component/
registry.rs

1//! Checked component registration table for one [`crate::world::World`].
2//!
3//! Typed and untyped tag registration share conflict detection for names, layouts, and storage
4//! policy before dense [`ComponentId`] handles are issued.
5
6use alloc::format;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9use core::any::{type_name, TypeId};
10use core::mem::{align_of, needs_drop, size_of};
11
12use crate::component::{ComponentOptions, StorageKind};
13use crate::world::WorldOwner;
14
15/// Dense registry-local component handle scoped to one world owner.
16#[derive(Clone, Debug, Eq, PartialEq, Hash)]
17pub struct ComponentId {
18    owner: WorldOwner,
19    index: u32,
20}
21
22/// Component registration conflict or policy violation.
23#[non_exhaustive]
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum RegistrationError {
26    /// The same `TypeId` was registered with incompatible metadata.
27    TypeConflict {
28        /// Registration name associated with the conflict.
29        name: String,
30        /// Existing entry name.
31        existing: String,
32        /// Requested entry name.
33        requested: String,
34    },
35    /// The registration name is already bound to different metadata.
36    NameConflict {
37        /// Registration name associated with the conflict.
38        name: String,
39        /// Existing entry name.
40        existing: String,
41        /// Requested entry name.
42        requested: String,
43    },
44    /// Size, alignment, or owner metadata does not match an existing entry.
45    LayoutConflict {
46        /// Registration name associated with the conflict.
47        name: String,
48        /// Human-readable layout detail.
49        detail: String,
50    },
51    /// Typed tag registration violated zero-sized non-dropping requirements.
52    InvalidTag {
53        /// Registration name associated with the conflict.
54        name: String,
55        /// Human-readable validation detail.
56        detail: String,
57    },
58    /// Requested storage policy is incompatible with the component shape.
59    UnsupportedStorage {
60        /// Registration name associated with the conflict.
61        name: String,
62        /// Human-readable policy detail.
63        detail: String,
64    },
65}
66
67struct ComponentEntry {
68    name: String,
69    type_id: Option<TypeId>,
70    is_tag: bool,
71    storage: StorageKind,
72    size: usize,
73    align: usize,
74}
75
76/// Checked component registration table.
77pub(crate) struct ComponentRegistry {
78    entries: Vec<ComponentEntry>,
79}
80
81impl ComponentId {
82    pub(crate) fn new(owner: WorldOwner, index: u32) -> Self {
83        Self { owner, index }
84    }
85
86    /// Dense registry index for diagnostics and lifecycle event wiring.
87    pub fn index(&self) -> usize {
88        self.index as usize
89    }
90
91    pub(crate) fn validate_owner(&self, owner: &WorldOwner) -> Result<(), RegistrationError> {
92        if self.owner.same(owner) {
93            Ok(())
94        } else {
95            Err(RegistrationError::LayoutConflict {
96                name: String::from("<component>"),
97                detail: String::from("component id belongs to a different world"),
98            })
99        }
100    }
101}
102
103impl ComponentRegistry {
104    pub fn new() -> Self {
105        Self {
106            entries: Vec::new(),
107        }
108    }
109
110    pub fn len(&self) -> usize {
111        self.entries.len()
112    }
113
114    pub fn register_typed<T: 'static>(
115        &mut self,
116        owner: &WorldOwner,
117        name: Option<&str>,
118        options: ComponentOptions,
119    ) -> Result<ComponentId, RegistrationError> {
120        let name = name.unwrap_or(type_name::<T>()).to_string();
121        if options.is_tag() {
122            Self::validate_typed_tag::<T>(&name)?;
123        }
124        if options.storage() == StorageKind::Table && options.is_tag() {
125            return Err(RegistrationError::UnsupportedStorage {
126                name,
127                detail: String::from("tag components cannot use table storage"),
128            });
129        }
130        self.register_inner(
131            owner,
132            name,
133            Some(TypeId::of::<T>()),
134            options,
135            size_of::<T>(),
136            align_of::<T>(),
137        )
138    }
139
140    pub fn register_untyped(
141        &mut self,
142        owner: &WorldOwner,
143        name: &str,
144        options: ComponentOptions,
145    ) -> Result<ComponentId, RegistrationError> {
146        if !options.is_tag() {
147            return Err(RegistrationError::LayoutConflict {
148                name: name.to_string(),
149                detail: String::from("untyped registration requires tag options"),
150            });
151        }
152        if options.storage() == StorageKind::Table {
153            return Err(RegistrationError::UnsupportedStorage {
154                name: name.to_string(),
155                detail: String::from("untyped tags cannot use table storage"),
156            });
157        }
158        self.register_inner(owner, name.to_string(), None, options, 0, 1)
159    }
160
161    pub fn storage_kind(&self, id: &ComponentId) -> Option<StorageKind> {
162        self.entries.get(id.index()).map(|entry| entry.storage)
163    }
164
165    #[allow(dead_code)]
166    pub fn is_tag(&self, id: &ComponentId) -> Option<bool> {
167        self.entries.get(id.index()).map(|entry| entry.is_tag)
168    }
169
170    pub(crate) fn entry_is_tag(&self, index: usize) -> bool {
171        self.entries.get(index).is_some_and(|entry| entry.is_tag)
172    }
173
174    pub(crate) fn entry_is_table(&self, index: usize) -> bool {
175        self.entries
176            .get(index)
177            .is_some_and(|entry| entry.storage == StorageKind::Table)
178    }
179
180    pub(crate) fn component_name(&self, id: &ComponentId) -> String {
181        self.entries
182            .get(id.index())
183            .map(|entry| entry.name.clone())
184            .unwrap_or_else(|| String::from("<unknown component>"))
185    }
186
187    pub(crate) fn type_id_for_index(&self, index: usize) -> Option<TypeId> {
188        self.entries.get(index).and_then(|entry| entry.type_id)
189    }
190
191    pub(crate) fn index_of_type(&self, type_id: TypeId) -> Option<usize> {
192        self.entries
193            .iter()
194            .position(|entry| entry.type_id == Some(type_id))
195    }
196
197    pub(crate) fn id_of<T: 'static>(&self, owner: &WorldOwner) -> Option<ComponentId> {
198        let type_id = TypeId::of::<T>();
199        self.entries
200            .iter()
201            .position(|entry| entry.type_id == Some(type_id))
202            .map(|index| ComponentId::new(owner.clone(), index as u32))
203    }
204
205    fn register_inner(
206        &mut self,
207        owner: &WorldOwner,
208        name: String,
209        type_id: Option<TypeId>,
210        options: ComponentOptions,
211        size: usize,
212        align: usize,
213    ) -> Result<ComponentId, RegistrationError> {
214        if let Some(existing) = self.find_exact(type_id, &name, options, size, align) {
215            return Ok(ComponentId::new(owner.clone(), existing as u32));
216        }
217
218        if let Some((index, reason)) = self.find_conflict(type_id, &name, options, size, align) {
219            let entry = &self.entries[index];
220            return Err(match reason {
221                ConflictKind::Type => RegistrationError::TypeConflict {
222                    name: name.clone(),
223                    existing: entry.name.clone(),
224                    requested: name,
225                },
226                ConflictKind::Name => RegistrationError::NameConflict {
227                    name: name.clone(),
228                    existing: entry.name.clone(),
229                    requested: name,
230                },
231                ConflictKind::Layout => RegistrationError::LayoutConflict {
232                    name: name.clone(),
233                    detail: format!(
234                        "existing={}:{}x{} {:?} {:?}; requested={}:{}x{} {:?} {:?}",
235                        entry.name,
236                        entry.size,
237                        entry.align,
238                        entry.type_id,
239                        entry.storage,
240                        name,
241                        size,
242                        align,
243                        type_id,
244                        options.storage()
245                    ),
246                },
247            });
248        }
249
250        let index = self.entries.len() as u32;
251        self.entries.push(ComponentEntry {
252            name,
253            type_id,
254            is_tag: options.is_tag(),
255            storage: options.storage(),
256            size,
257            align,
258        });
259        Ok(ComponentId::new(owner.clone(), index))
260    }
261
262    fn find_exact(
263        &self,
264        type_id: Option<TypeId>,
265        name: &str,
266        options: ComponentOptions,
267        size: usize,
268        align: usize,
269    ) -> Option<usize> {
270        self.entries.iter().position(|entry| {
271            entry.name == name
272                && entry.type_id == type_id
273                && entry.is_tag == options.is_tag()
274                && entry.storage == options.storage()
275                && entry.size == size
276                && entry.align == align
277        })
278    }
279
280    fn find_conflict(
281        &self,
282        type_id: Option<TypeId>,
283        name: &str,
284        options: ComponentOptions,
285        size: usize,
286        align: usize,
287    ) -> Option<(usize, ConflictKind)> {
288        for (index, entry) in self.entries.iter().enumerate() {
289            if entry.type_id == type_id && type_id.is_some() {
290                if entry.size != size || entry.align != align {
291                    return Some((index, ConflictKind::Layout));
292                } else if entry.name != name
293                    || entry.is_tag != options.is_tag()
294                    || entry.storage != options.storage()
295                {
296                    return Some((index, ConflictKind::Type));
297                }
298            } else if entry.name == name
299                && (entry.type_id != type_id
300                    || entry.is_tag != options.is_tag()
301                    || entry.storage != options.storage()
302                    || entry.size != size
303                    || entry.align != align)
304            {
305                return Some((index, ConflictKind::Name));
306            }
307        }
308        None
309    }
310
311    #[cfg(test)]
312    pub(crate) fn find_conflict_for_test(
313        &self,
314        type_id: Option<TypeId>,
315        name: &str,
316        options: ComponentOptions,
317        size: usize,
318        align: usize,
319    ) -> bool {
320        self.find_conflict(type_id, name, options, size, align)
321            .is_some()
322    }
323
324    fn validate_typed_tag<T: 'static>(name: &str) -> Result<(), RegistrationError> {
325        if size_of::<T>() != 0 || needs_drop::<T>() {
326            return Err(RegistrationError::InvalidTag {
327                name: name.to_string(),
328                detail: String::from("typed tag components must be zero-sized and non-dropping"),
329            });
330        }
331        Ok(())
332    }
333}
334
335enum ConflictKind {
336    Type,
337    Name,
338    Layout,
339}
340
341impl Default for ComponentRegistry {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347#[cfg(feature = "std")]
348impl core::fmt::Display for RegistrationError {
349    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
350        match self {
351            Self::TypeConflict {
352                name,
353                existing,
354                requested,
355            } => write!(
356                f,
357                "component type conflict for {name}: existing={existing}, requested={requested}"
358            ),
359            Self::NameConflict {
360                name,
361                existing,
362                requested,
363            } => write!(
364                f,
365                "component name conflict for {name}: existing={existing}, requested={requested}"
366            ),
367            Self::LayoutConflict { name, detail } => {
368                write!(f, "component layout conflict for {name}: {detail}")
369            }
370            Self::InvalidTag { name, detail } => {
371                write!(f, "invalid tag component {name}: {detail}")
372            }
373            Self::UnsupportedStorage { name, detail } => {
374                write!(f, "unsupported storage for {name}: {detail}")
375            }
376        }
377    }
378}
379
380#[cfg(feature = "std")]
381impl std::error::Error for RegistrationError {}
382
383#[cfg(test)]
384mod tests;
385
386#[cfg(test)]
387mod default_tests {
388    use super::ComponentRegistry;
389
390    #[test]
391    fn default_registry_is_empty() {
392        assert_eq!(ComponentRegistry::default().len(), 0);
393    }
394}