Repository: Ralith/hecs
Pull request #89 (2020-10-16 00:19:12)

Adds spawn_at API to World.
`spawn_at` takes an additional parameter of an Entity, to spawn the
corresponding entity at.  This can be useful if you're saving and
restoring gamestate in memory.  This can also be used to resurrect dead
entities references at will, presuming you have an old handle laying
around.

googlebot: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2FRalith%2Fhecs%2Fpull%2F89) for more info**.

<!-- need_author_consent -->
Ralith: Needs rebase, and I'm guessing googlebot's confused by email addresses not quite lining up.

Crazy idea: what if we named this `resurrect`?
AngelOfSol: I thinkI think the CLA issue is because I based it off the other PR and the other commit is not author'd by me.  Resurrect is an interesting API naming choice, as it could be used as that and it's not exactly wrong per se, but on an empty world, there are no enitties to resurrect.  
Ralith: The rebase was not performed correctly.
AngelOfSol: > The rebase was not performed correctly.

Yeah I can see that, not used to the whole PR process sorry!  I'll fix it.
googlebot: CLAs look good, thanks!

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2FRalith%2Fhecs%2Fpull%2F89) for more info**.

<!-- ok -->

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 21a40c5..8b71141 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ lazy_static = { version = "1.4.0", optional = true, features = ["spin_no_std"] }
 [dev-dependencies]
 bencher = "0.1.5"
 rand = "0.7.3"
+trybuild = "1.0.23"
 
 [[bench]]
 name = "bench"
@@ -38,3 +39,6 @@ required-features = ["macros"]
 
 [profile.release]
 debug = true
+
+[workspace]
+members = ["macros"]
\ No newline at end of file
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 5b88d18..483aa1f 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -14,10 +14,13 @@
 
 extern crate proc_macro;
 
+use std::borrow::Cow;
+
 use proc_macro::TokenStream;
 use proc_macro2::Span;
+use proc_macro2::TokenStream as TokenStream2;
 use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error, Result};
 
 /// Implement `Bundle` for a monomorphic struct
 ///
@@ -29,104 +32,201 @@ use syn::{parse_macro_input, DeriveInput};
 #[proc_macro_derive(Bundle)]
 pub fn derive_bundle(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
-    if !input.generics.params.is_empty() {
-        return TokenStream::from(
-            quote! { compile_error!("derive(Bundle) does not support generics"); },
-        );
+    match derive_bundle_(input) {
+        Ok(ts) => ts,
+        Err(e) => e.to_compile_error(),
     }
+    .into()
+}
+
+fn derive_bundle_(input: DeriveInput) -> Result<TokenStream2> {
+    let ident = input.ident;
     let data = match input.data {
         syn::Data::Struct(s) => s,
         _ => {
-            return TokenStream::from(
-                quote! { compile_error!("derive(Bundle) only supports structs"); },
-            )
+            return Err(Error::new_spanned(
+                ident,
+                "derive(Bundle) does not support enums or unions",
+            ))
         }
     };
-    let ident = input.ident;
-    let (tys, fields) = struct_fields(&data.fields);
+    let (tys, field_members) = struct_fields(&data.fields);
+    let field_idents = member_as_idents(&field_members);
+    let generics = add_additional_bounds_to_generic_params(input.generics);
 
-    let n = tys.len();
-    let code = quote! {
-        impl ::hecs::DynamicBundle for #ident {
-            fn with_ids<T>(&self, f: impl FnOnce(&[std::any::TypeId]) -> T) -> T {
-                Self::with_static_ids(f)
+    let dyn_bundle_code = gen_dynamic_bundle_impl(&ident, &generics, &field_members, &tys);
+    let bundle_code = if tys.is_empty() {
+        gen_unit_struct_bundle_impl(ident, &generics)
+    } else {
+        gen_bundle_impl(&ident, &generics, &field_members, &field_idents, &tys)
+    };
+    let mut ts = dyn_bundle_code;
+    ts.extend(bundle_code);
+    Ok(ts)
+}
+
+fn gen_dynamic_bundle_impl(
+    ident: &syn::Ident,
+    generics: &syn::Generics,
+    field_members: &[syn::Member],
+    tys: &[&syn::Type],
+) -> TokenStream2 {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
+        impl #impl_generics ::hecs::DynamicBundle for #ident #ty_generics #where_clause {
+            fn with_ids<__hecs__T>(&self, f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
+                <Self as ::hecs::Bundle>::with_static_ids(f)
             }
 
-            fn type_info(&self) -> Vec<::hecs::TypeInfo> {
-                Self::static_type_info()
+            fn type_info(&self) -> ::std::vec::Vec<::hecs::TypeInfo> {
+                <Self as ::hecs::Bundle>::static_type_info()
             }
 
             #[allow(clippy::forget_copy)]
-            unsafe fn put(mut self, mut f: impl FnMut(*mut u8, ::hecs::TypeInfo)) {
+            unsafe fn put(mut self, mut f: impl ::std::ops::FnMut(*mut u8, ::hecs::TypeInfo)) {
                 #(
-                    f((&mut self.#fields as *mut #tys).cast::<u8>(), ::hecs::TypeInfo::of::<#tys>());
-                    std::mem::forget(self.#fields);
+                    f((&mut self.#field_members as *mut #tys).cast::<u8>(), ::hecs::TypeInfo::of::<#tys>());
+                    ::std::mem::forget(self.#field_members);
                 )*
             }
         }
+    }
+}
 
-        impl ::hecs::Bundle for #ident {
-            fn with_static_ids<T>(f: impl FnOnce(&[std::any::TypeId]) -> T) -> T {
-                use std::any::TypeId;
-                use std::mem;
-
-                ::hecs::lazy_static::lazy_static! {
-                    static ref ELEMENTS: [TypeId; #n] = {
-                        let mut dedup = std::collections::HashSet::new();
-                        for &(ty, name) in [#((std::any::TypeId::of::<#tys>(), std::any::type_name::<#tys>())),*].iter() {
-                            if !dedup.insert(ty) {
-                                panic!("{} has multiple {} fields; each type must occur at most once!", stringify!(#ident), name);
-                            }
-                        }
-
-                        let mut tys = [#((mem::align_of::<#tys>(), TypeId::of::<#tys>())),*];
-                        tys.sort_unstable_by(|x, y| x.0.cmp(&y.0).reverse().then(x.1.cmp(&y.1)));
-                        let mut ids = [TypeId::of::<()>(); #n];
-                        for (id, info) in ids.iter_mut().zip(tys.iter()) {
-                            *id = info.1;
-                        }
-                        ids
-                    };
-                }
-
-                f(&*ELEMENTS)
+fn gen_bundle_impl(
+    ident: &syn::Ident,
+    generics: &syn::Generics,
+    field_members: &[syn::Member],
+    field_idents: &[Cow<syn::Ident>],
+    tys: &[&syn::Type],
+) -> TokenStream2 {
+    let num_tys = tys.len();
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    let with_static_ids_inner = quote! {
+        {
+            let mut tys = [#((::std::mem::align_of::<#tys>(), ::std::any::TypeId::of::<#tys>())),*];
+            tys.sort_unstable_by(|x, y| {
+                ::std::cmp::Ord::cmp(&x.0, &y.0)
+                    .reverse()
+                    .then(::std::cmp::Ord::cmp(&x.1, &y.1))
+            });
+            let mut ids = [::std::any::TypeId::of::<()>(); #num_tys];
+            for (id, info) in ::std::iter::Iterator::zip(ids.iter_mut(), tys.iter()) {
+                *id = info.1;
+            }
+            ids
+        }
+    };
+    let with_static_ids_body = if generics.params.is_empty() {
+        quote! {
+            ::hecs::lazy_static::lazy_static! {
+                static ref ELEMENTS: [::std::any::TypeId; #num_tys] = {
+                    #with_static_ids_inner
+                };
+            }
+            f(&*ELEMENTS)
+        }
+    } else {
+        quote! {
+            f(&#with_static_ids_inner)
+        }
+    };
+    quote! {
+        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
+            #[allow(non_camel_case_types)]
+            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
+                #with_static_ids_body
             }
 
-            fn static_type_info() -> Vec<::hecs::TypeInfo> {
-                let mut info = vec![#(::hecs::TypeInfo::of::<#tys>()),*];
+            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> {
+                let mut info = ::std::vec![#(::hecs::TypeInfo::of::<#tys>()),*];
                 info.sort_unstable();
                 info
             }
 
             unsafe fn get(
-                mut f: impl FnMut(::hecs::TypeInfo) -> Option<std::ptr::NonNull<u8>>,
-            ) -> Result<Self, ::hecs::MissingComponent> {
+                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> ::std::option::Option<::std::ptr::NonNull<u8>>,
+            ) -> ::std::result::Result<Self, ::hecs::MissingComponent> {
                 #(
-                    let #fields = f(::hecs::TypeInfo::of::<#tys>())
+                    let #field_idents = f(::hecs::TypeInfo::of::<#tys>())
                             .ok_or_else(::hecs::MissingComponent::new::<#tys>)?
                             .cast::<#tys>()
-                        .as_ptr();
+                            .as_ptr();
                 )*
-                Ok(Self { #( #fields: #fields.read(), )* })
+                ::std::result::Result::Ok(Self { #( #field_members: #field_idents.read(), )* })
             }
         }
-    };
-    TokenStream::from(code)
+    }
+}
+
+// no reason to generate a static for unit structs
+fn gen_unit_struct_bundle_impl(ident: syn::Ident, generics: &syn::Generics) -> TokenStream2 {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
+        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
+            #[allow(non_camel_case_types)]
+            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T { f(&[]) }
+            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> { ::std::vec::Vec::new() }
+
+            unsafe fn get(
+                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> Option<::std::ptr::NonNull<u8>>,
+            ) -> Result<Self, ::hecs::MissingComponent> {
+                Ok(Self {/* for some reason this works for all unit struct variations */})
+            }
+        }
+    }
 }
 
-fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Ident>) {
+fn make_component_trait_bound() -> syn::TraitBound {
+    syn::TraitBound {
+        paren_token: None,
+        modifier: syn::TraitBoundModifier::None,
+        lifetimes: None,
+        path: syn::parse_quote!(::hecs::Component),
+    }
+}
+
+fn add_additional_bounds_to_generic_params(mut generics: syn::Generics) -> syn::Generics {
+    generics.type_params_mut().for_each(|tp| {
+        tp.bounds
+            .push(syn::TypeParamBound::Trait(make_component_trait_bound()))
+    });
+    generics
+}
+
+fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Member>) {
     match fields {
         syn::Fields::Named(ref fields) => fields
             .named
             .iter()
-            .map(|f| (&f.ty, f.ident.clone().unwrap()))
+            .map(|f| (&f.ty, syn::Member::Named(f.ident.clone().unwrap())))
             .unzip(),
         syn::Fields::Unnamed(ref fields) => fields
             .unnamed
             .iter()
             .enumerate()
-            .map(|(i, f)| (&f.ty, syn::Ident::new(&i.to_string(), Span::call_site())))
+            .map(|(i, f)| {
+                (
+                    &f.ty,
+                    syn::Member::Unnamed(syn::Index {
+                        index: i as u32,
+                        span: Span::call_site(),
+                    }),
+                )
+            })
             .unzip(),
         syn::Fields::Unit => (Vec::new(), Vec::new()),
     }
 }
+
+fn member_as_idents<'a>(members: &'a [syn::Member]) -> Vec<Cow<'a, syn::Ident>> {
+    members
+        .iter()
+        .map(|member| match member {
+            syn::Member::Named(ident) => Cow::Borrowed(ident),
+            &syn::Member::Unnamed(syn::Index { index, span }) => {
+                Cow::Owned(syn::Ident::new(&format!("tuple_field_{}", index), span))
+            }
+        })
+        .collect()
+}
diff --git a/src/archetype.rs b/src/archetype.rs
index 91e0389..f8c21c0 100644
--- a/src/archetype.rs
+++ b/src/archetype.rs
@@ -17,10 +17,11 @@ use crate::alloc::boxed::Box;
 use crate::alloc::{vec, vec::Vec};
 use core::any::{type_name, TypeId};
 use core::cell::UnsafeCell;
+use core::hash::{BuildHasher, BuildHasherDefault, Hasher};
 use core::mem;
 use core::ptr::{self, NonNull};
 
-use hashbrown::HashMap;
+use hashbrown::{hash_map::DefaultHashBuilder, HashMap};
 
 use crate::borrow::AtomicBorrow;
 use crate::query::Fetch;
@@ -32,7 +33,7 @@ use crate::{Access, Component, Query};
 /// go through the `World`.
 pub struct Archetype {
     types: Vec<TypeInfo>,
-    state: HashMap<TypeId, TypeState>,
+    state: TypeIdMap<TypeState>,
     len: u32,
     entities: Box<[u32]>,
     // UnsafeCell allows unique references into `data` to be constructed while shared references
@@ -42,11 +43,26 @@ pub struct Archetype {
 }
 
 impl Archetype {
+    fn assert_type_info(types: &[TypeInfo]) {
+        types.windows(2).for_each(|x| match x[0].cmp(&x[1]) {
+            core::cmp::Ordering::Less => (),
+            #[cfg(debug_assertions)]
+            core::cmp::Ordering::Equal => panic!(
+                "attempted to allocate entity with duplicate {} components; \
+                 each type must occur at most once!",
+                x[0].type_name
+            ),
+            #[cfg(not(debug_assertions))]
+            core::cmp::Ordering::Equal => panic!(
+                "attempted to allocate entity with duplicate components; \
+                 each type must occur at most once!"
+            ),
+            core::cmp::Ordering::Greater => panic!("type info is unsorted"),
+        });
+    }
+
     pub(crate) fn new(types: Vec<TypeInfo>) -> Self {
-        debug_assert!(
-            types.windows(2).all(|x| x[0] < x[1]),
-            "type info unsorted or contains duplicates"
-        );
+        Self::assert_type_info(&types);
         Self {
             types,
             state: HashMap::default(),
@@ -121,10 +137,12 @@ impl Archetype {
         }
     }
 
+    #[inline]
     pub(crate) fn len(&self) -> u32 {
         self.len
     }
 
+    #[inline]
     pub(crate) fn entities(&self) -> NonNull<u32> {
         unsafe { NonNull::new_unchecked(self.entities.as_ptr() as *mut _) }
     }
@@ -198,7 +216,7 @@ impl Archetype {
             self.entities = new_entities;
 
             let old_data_size = mem::replace(&mut self.data_size, 0);
-            let mut state = HashMap::with_capacity(self.types.len());
+            let mut state = HashMap::with_capacity_and_hasher(self.types.len(), Default::default());
             for ty in &self.types {
                 self.data_size = align(self.data_size, ty.layout.align());
                 state.insert(ty.id, TypeState::new(self.data_size));
@@ -338,6 +356,50 @@ impl Drop for Archetype {
     }
 }
 
+/// A hasher optimized for hashing a single TypeId.
+///
+/// TypeId is already thoroughly hashed, so there's no reason to hash it again.
+/// Just leave the bits unchanged.
+#[derive(Default)]
+pub(crate) struct TypeIdHasher {
+    hash: u64,
+}
+
+impl Hasher for TypeIdHasher {
+    fn write_u64(&mut self, n: u64) {
+        // Only a single value can be hashed, so the old hash should be zero.
+        debug_assert_eq!(self.hash, 0);
+        self.hash = n;
+    }
+
+    // Tolerate TypeId being either u64 or u128.
+    fn write_u128(&mut self, n: u128) {
+        debug_assert_eq!(self.hash, 0);
+        self.hash = n as u64;
+    }
+
+    fn write(&mut self, bytes: &[u8]) {
+        debug_assert_eq!(self.hash, 0);
+
+        // This will only be called if TypeId is neither u64 nor u128, which is not anticipated.
+        // In that case we'll just fall back to using a different hash implementation.
+        let mut hasher = <DefaultHashBuilder as BuildHasher>::Hasher::default();
+        hasher.write(bytes);
+        self.hash = hasher.finish();
+    }
+
+    fn finish(&self) -> u64 {
+        self.hash
+    }
+}
+
+/// A HashMap with TypeId keys
+///
+/// Because TypeId is already a fully-hashed u64 (including data in the high seven bits,
+/// which hashbrown needs), there is no need to hash it again. Instead, this uses the much
+/// faster no-op hash.
+pub(crate) type TypeIdMap<V> = HashMap<TypeId, V, BuildHasherDefault<TypeIdHasher>>;
+
 struct TypeState {
     offset: usize,
     borrow: AtomicBorrow,
@@ -358,6 +420,8 @@ pub struct TypeInfo {
     id: TypeId,
     layout: Layout,
     drop: unsafe fn(*mut u8),
+    #[cfg(debug_assertions)]
+    type_name: &'static str,
 }
 
 impl TypeInfo {
@@ -371,6 +435,8 @@ impl TypeInfo {
             id: TypeId::of::<T>(),
             layout: Layout::new::<T>(),
             drop: drop_ptr::<T>,
+            #[cfg(debug_assertions)]
+            type_name: core::any::type_name::<T>(),
         }
     }
 
diff --git a/src/entities.rs b/src/entities.rs
index 47a014d..ec662c2 100644
--- a/src/entities.rs
+++ b/src/entities.rs
@@ -224,6 +224,35 @@ impl Entities {
         }
     }
 
+    /// Allocate a specific entity ID, overwriting its generation
+    ///
+    /// Returns the location of the entity currently using the given ID, if any. Location should be written immediately.
+    pub fn alloc_at(&mut self, entity: Entity) -> Option<Location> {
+        self.verify_flushed();
+
+        let loc = if entity.id as usize >= self.meta.len() {
+            self.pending.extend((self.meta.len() as u32)..entity.id);
+            let new_free_cursor = self.pending.len() as i64;
+            self.free_cursor.store(new_free_cursor, Ordering::Relaxed); // Not racey due to &mut self
+            self.meta.resize(entity.id as usize + 1, EntityMeta::EMPTY);
+            None
+        } else if let Some(index) = self.pending.iter().position(|item| *item == entity.id) {
+            self.pending.swap_remove(index);
+            let new_free_cursor = self.pending.len() as i64;
+            self.free_cursor.store(new_free_cursor, Ordering::Relaxed); // Not racey due to &mut self
+            None
+        } else {
+            Some(mem::replace(
+                &mut self.meta[entity.id as usize].location,
+                EntityMeta::EMPTY.location,
+            ))
+        };
+
+        self.meta[entity.id as usize].generation = entity.generation;
+
+        loc
+    }
+
     /// Destroy an entity, allowing it to be reused
     ///
     /// Must not be called while reserved entities are awaiting `flush()`.
@@ -455,6 +484,50 @@ mod tests {
         }
     }
 
+    #[test]
+    fn alloc_at() {
+        let mut e = Entities::default();
+
+        let mut old = Vec::new();
+
+        for _ in 0..2 {
+            let entity = e.alloc();
+            old.push(entity);
+            e.free(entity).unwrap();
+        }
+
+        let id = old.first().unwrap().id();
+        assert!(old.iter().all(|entity| entity.id() == id));
+
+        let entity = *old.last().unwrap();
+        // The old ID shouldn't exist at this point, and should exist
+        // in the pending list.
+        assert!(!e.contains(entity));
+        assert!(e.pending.contains(&entity.id()));
+        // Allocating an entity at an unused location should not cause a location to be returned.
+        assert!(e.alloc_at(entity).is_none());
+        assert!(e.contains(entity));
+        // The entity in question should not exist in the free-list once allocated.
+        assert!(!e.pending.contains(&entity.id()));
+        // Allocating at the same id again should cause a location to be returned
+        // this time around.
+        assert!(e.alloc_at(entity).is_some());
+        assert!(e.contains(entity));
+
+        // Allocating an Entity should cause the new empty locations
+        // to be located in the free list.
+        assert_eq!(e.meta.len(), 1);
+        assert!(e
+            .alloc_at(Entity {
+                id: 3,
+                generation: 2,
+            })
+            .is_none());
+        assert_eq!(e.pending.len(), 2);
+        assert_eq!(&e.pending, &[1, 2]);
+        assert_eq!(e.meta.len(), 4);
+    }
+
     #[test]
     fn contains() {
         let mut e = Entities::default();
diff --git a/src/entity_builder.rs b/src/entity_builder.rs
index 0c15f0f..6d1fa5d 100644
--- a/src/entity_builder.rs
+++ b/src/entity_builder.rs
@@ -19,9 +19,9 @@ use core::any::TypeId;
 use core::mem::{self, MaybeUninit};
 use core::ptr;
 
-use hashbrown::hash_map::{Entry, HashMap};
+use hashbrown::hash_map::Entry;
 
-use crate::archetype::TypeInfo;
+use crate::archetype::{TypeIdMap, TypeInfo};
 use crate::{Component, DynamicBundle};
 
 /// Helper for incrementally constructing a bundle of components with dynamic component types
@@ -42,7 +42,7 @@ pub struct EntityBuilder {
     cursor: usize,
     info: Vec<(TypeInfo, usize)>,
     ids: Vec<TypeId>,
-    indices: HashMap<TypeId, usize>,
+    indices: TypeIdMap<usize>,
 }
 
 impl EntityBuilder {
@@ -53,7 +53,7 @@ impl EntityBuilder {
             storage: Box::new([]),
             info: Vec::new(),
             ids: Vec::new(),
-            indices: HashMap::new(),
+            indices: Default::default(),
         }
     }
 
diff --git a/src/query.rs b/src/query.rs
index e635e33..5b63cca 100644
--- a/src/query.rs
+++ b/src/query.rs
@@ -30,27 +30,27 @@ pub trait Fetch<'a>: Sized {
     /// Type of value to be fetched
     type Item;
 
+    /// A value on which `get` may never be called
+    fn dangling() -> Self;
+
     /// How this query will access `archetype`, if at all
     fn access(archetype: &Archetype) -> Option<Access>;
 
     /// Acquire dynamic borrows from `archetype`
     fn borrow(archetype: &Archetype);
     /// Construct a `Fetch` for `archetype` if it should be traversed
-    ///
-    /// # Safety
-    /// `offset` must be in bounds of `archetype`
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self>;
+    fn new(archetype: &'a Archetype) -> Option<Self>;
     /// Release dynamic borrows acquired by `borrow`
     fn release(archetype: &Archetype);
 
-    /// Access the next item in this archetype without bounds checking
+    /// Access the `n`th item in this archetype without bounds checking
     ///
     /// # Safety
     /// - Must only be called after `borrow`
     /// - `release` must not be called while `'a` is still live
     /// - Bounds-checking must be performed externally
     /// - Any resulting borrows must be legal (e.g. no &mut to something another iterator might access)
-    unsafe fn next(&mut self) -> Self::Item;
+    unsafe fn get(&self, n: usize) -> Self::Item;
 }
 
 /// Type of access a `Query` may have to an `Archetype`
@@ -74,6 +74,10 @@ pub struct FetchRead<T>(NonNull<T>);
 impl<'a, T: Component> Fetch<'a> for FetchRead<T> {
     type Item = &'a T;
 
+    fn dangling() -> Self {
+        Self(NonNull::dangling())
+    }
+
     fn access(archetype: &Archetype) -> Option<Access> {
         if archetype.has::<T>() {
             Some(Access::Read)
@@ -85,19 +89,15 @@ impl<'a, T: Component> Fetch<'a> for FetchRead<T> {
     fn borrow(archetype: &Archetype) {
         archetype.borrow::<T>();
     }
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
-        archetype
-            .get::<T>()
-            .map(|x| Self(NonNull::new_unchecked(x.as_ptr().add(offset))))
+    fn new(archetype: &'a Archetype) -> Option<Self> {
+        archetype.get::<T>().map(Self)
     }
     fn release(archetype: &Archetype) {
         archetype.release::<T>();
     }
 
-    unsafe fn next(&mut self) -> &'a T {
-        let x = self.0.as_ptr();
-        self.0 = NonNull::new_unchecked(x.add(1));
-        &*x
+    unsafe fn get(&self, n: usize) -> Self::Item {
+        &*self.0.as_ptr().add(n)
     }
 }
 
@@ -111,6 +111,10 @@ pub struct FetchWrite<T>(NonNull<T>);
 impl<'a, T: Component> Fetch<'a> for FetchWrite<T> {
     type Item = &'a mut T;
 
+    fn dangling() -> Self {
+        Self(NonNull::dangling())
+    }
+
     fn access(archetype: &Archetype) -> Option<Access> {
         if archetype.has::<T>() {
             Some(Access::Write)
@@ -122,19 +126,15 @@ impl<'a, T: Component> Fetch<'a> for FetchWrite<T> {
     fn borrow(archetype: &Archetype) {
         archetype.borrow_mut::<T>();
     }
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
-        archetype
-            .get::<T>()
-            .map(|x| Self(NonNull::new_unchecked(x.as_ptr().add(offset))))
+    fn new(archetype: &'a Archetype) -> Option<Self> {
+        archetype.get::<T>().map(Self)
     }
     fn release(archetype: &Archetype) {
         archetype.release_mut::<T>();
     }
 
-    unsafe fn next(&mut self) -> &'a mut T {
-        let x = self.0.as_ptr();
-        self.0 = NonNull::new_unchecked(x.add(1));
-        &mut *x
+    unsafe fn get(&self, n: usize) -> Self::Item {
+        &mut *self.0.as_ptr().add(n)
     }
 }
 
@@ -148,6 +148,10 @@ pub struct TryFetch<T>(Option<T>);
 impl<'a, T: Fetch<'a>> Fetch<'a> for TryFetch<T> {
     type Item = Option<T::Item>;
 
+    fn dangling() -> Self {
+        Self(None)
+    }
+
     fn access(archetype: &Archetype) -> Option<Access> {
         Some(T::access(archetype).unwrap_or(Access::Iterate))
     }
@@ -155,15 +159,15 @@ impl<'a, T: Fetch<'a>> Fetch<'a> for TryFetch<T> {
     fn borrow(archetype: &Archetype) {
         T::borrow(archetype)
     }
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
-        Some(Self(T::get(archetype, offset)))
+    fn new(archetype: &'a Archetype) -> Option<Self> {
+        Some(Self(T::new(archetype)))
     }
     fn release(archetype: &Archetype) {
         T::release(archetype)
     }
 
-    unsafe fn next(&mut self) -> Option<T::Item> {
-        Some(self.0.as_mut()?.next())
+    unsafe fn get(&self, n: usize) -> Option<T::Item> {
+        Some(self.0.as_ref()?.get(n))
     }
 }
 
@@ -196,6 +200,10 @@ pub struct FetchWithout<T, F>(F, PhantomData<fn(T)>);
 impl<'a, T: Component, F: Fetch<'a>> Fetch<'a> for FetchWithout<T, F> {
     type Item = F::Item;
 
+    fn dangling() -> Self {
+        Self(F::dangling(), PhantomData)
+    }
+
     fn access(archetype: &Archetype) -> Option<Access> {
         if archetype.has::<T>() {
             None
@@ -207,18 +215,18 @@ impl<'a, T: Component, F: Fetch<'a>> Fetch<'a> for FetchWithout<T, F> {
     fn borrow(archetype: &Archetype) {
         F::borrow(archetype)
     }
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
+    fn new(archetype: &'a Archetype) -> Option<Self> {
         if archetype.has::<T>() {
             return None;
         }
-        Some(Self(F::get(archetype, offset)?, PhantomData))
+        Some(Self(F::new(archetype)?, PhantomData))
     }
     fn release(archetype: &Archetype) {
         F::release(archetype)
     }
 
-    unsafe fn next(&mut self) -> F::Item {
-        self.0.next()
+    unsafe fn get(&self, n: usize) -> F::Item {
+        self.0.get(n)
     }
 }
 
@@ -253,6 +261,10 @@ pub struct FetchWith<T, F>(F, PhantomData<fn(T)>);
 impl<'a, T: Component, F: Fetch<'a>> Fetch<'a> for FetchWith<T, F> {
     type Item = F::Item;
 
+    fn dangling() -> Self {
+        Self(F::dangling(), PhantomData)
+    }
+
     fn access(archetype: &Archetype) -> Option<Access> {
         if archetype.has::<T>() {
             F::access(archetype)
@@ -264,18 +276,18 @@ impl<'a, T: Component, F: Fetch<'a>> Fetch<'a> for FetchWith<T, F> {
     fn borrow(archetype: &Archetype) {
         F::borrow(archetype)
     }
-    unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
+    fn new(archetype: &'a Archetype) -> Option<Self> {
         if !archetype.has::<T>() {
             return None;
         }
-        Some(Self(F::get(archetype, offset)?, PhantomData))
+        Some(Self(F::new(archetype)?, PhantomData))
     }
     fn release(archetype: &Archetype) {
         F::release(archetype)
     }
 
-    unsafe fn next(&mut self) -> F::Item {
-        self.0.next()
+    unsafe fn get(&self, n: usize) -> F::Item {
+        self.0.get(n)
     }
 }
 
@@ -307,7 +319,7 @@ impl<'w, Q: Query> QueryBorrow<'w, Q> {
         QueryIter {
             borrow: self,
             archetype_index: 0,
-            iter: None,
+            iter: ChunkIter::empty(),
         }
     }
 
@@ -428,8 +440,8 @@ impl<'q, 'w, Q: Query> IntoIterator for &'q mut QueryBorrow<'w, Q> {
 /// Iterator over the set of entities with the components in `Q`
 pub struct QueryIter<'q, 'w, Q: Query> {
     borrow: &'q mut QueryBorrow<'w, Q>,
-    archetype_index: u32,
-    iter: Option<ChunkIter<Q>>,
+    archetype_index: usize,
+    iter: ChunkIter<Q>,
 }
 
 unsafe impl<'q, 'w, Q: Query> Send for QueryIter<'q, 'w, Q> {}
@@ -441,33 +453,30 @@ impl<'q, 'w, Q: Query> Iterator for QueryIter<'q, 'w, Q> {
     #[inline]
     fn next(&mut self) -> Option<Self::Item> {
         loop {
-            match self.iter {
+            match unsafe { self.iter.next() } {
                 None => {
-                    let archetype = self.borrow.archetypes.get(self.archetype_index as usize)?;
+                    let archetype = self.borrow.archetypes.get(self.archetype_index)?;
                     self.archetype_index += 1;
-                    unsafe {
-                        self.iter = Q::Fetch::get(archetype, 0).map(|fetch| ChunkIter {
+                    self.iter =
+                        Q::Fetch::new(archetype).map_or(ChunkIter::empty(), |fetch| ChunkIter {
                             entities: archetype.entities(),
                             fetch,
-                            len: archetype.len(),
+                            position: 0,
+                            len: archetype.len() as usize,
                         });
-                    }
+                    continue;
                 }
-                Some(ref mut iter) => match unsafe { iter.next() } {
-                    None => {
-                        self.iter = None;
-                        continue;
-                    }
-                    Some((id, components)) => {
-                        return Some((
-                            Entity {
-                                id,
-                                generation: self.borrow.meta[id as usize].generation,
+                Some((id, components)) => {
+                    return Some((
+                        Entity {
+                            id,
+                            generation: unsafe {
+                                self.borrow.meta.get_unchecked(id as usize).generation
                             },
-                            components,
-                        ));
-                    }
-                },
+                        },
+                        components,
+                    ));
+                }
             }
         }
     }
@@ -492,26 +501,36 @@ impl<'q, 'w, Q: Query> ExactSizeIterator for QueryIter<'q, 'w, Q> {
 struct ChunkIter<Q: Query> {
     entities: NonNull<u32>,
     fetch: Q::Fetch,
-    len: u32,
+    position: usize,
+    len: usize,
 }
 
 impl<Q: Query> ChunkIter<Q> {
+    fn empty() -> Self {
+        Self {
+            entities: NonNull::dangling(),
+            fetch: Q::Fetch::dangling(),
+            position: 0,
+            len: 0,
+        }
+    }
+
     #[inline]
     unsafe fn next<'a>(&mut self) -> Option<(u32, <Q::Fetch as Fetch<'a>>::Item)> {
-        if self.len == 0 {
+        if self.position == self.len {
             return None;
         }
-        self.len -= 1;
-        let entity = self.entities.as_ptr();
-        self.entities = NonNull::new_unchecked(entity.add(1));
-        Some((*entity, self.fetch.next()))
+        let entity = self.entities.as_ptr().add(self.position);
+        let item = self.fetch.get(self.position);
+        self.position += 1;
+        Some((*entity, item))
     }
 }
 
 /// Batched version of `QueryIter`
 pub struct BatchedIter<'q, 'w, Q: Query> {
     borrow: &'q mut QueryBorrow<'w, Q>,
-    archetype_index: u32,
+    archetype_index: usize,
     batch_size: u32,
     batch: u32,
 }
@@ -524,26 +543,23 @@ impl<'q, 'w, Q: Query> Iterator for BatchedIter<'q, 'w, Q> {
 
     fn next(&mut self) -> Option<Self::Item> {
         loop {
-            let archetype = self.borrow.archetypes.get(self.archetype_index as usize)?;
+            let archetype = self.borrow.archetypes.get(self.archetype_index)?;
             let offset = self.batch_size * self.batch;
             if offset >= archetype.len() {
                 self.archetype_index += 1;
                 self.batch = 0;
                 continue;
             }
-            if let Some(fetch) = unsafe { Q::Fetch::get(archetype, offset as usize) } {
+            if let Some(fetch) = Q::Fetch::new(archetype) {
                 self.batch += 1;
                 return Some(Batch {
                     _marker: PhantomData,
                     meta: self.borrow.meta,
                     state: ChunkIter {
-                        entities: unsafe {
-                            NonNull::new_unchecked(
-                                archetype.entities().as_ptr().add(offset as usize),
-                            )
-                        },
+                        entities: archetype.entities(),
                         fetch,
-                        len: self.batch_size.min(archetype.len() - offset),
+                        len: (offset + self.batch_size.min(archetype.len() - offset)) as usize,
+                        position: offset as usize,
                     },
                 });
             } else {
@@ -588,6 +604,10 @@ macro_rules! tuple_impl {
         impl<'a, $($name: Fetch<'a>),*> Fetch<'a> for ($($name,)*) {
             type Item = ($($name::Item,)*);
 
+            fn dangling() -> Self {
+                ($($name::dangling(),)*)
+            }
+
             #[allow(unused_variables, unused_mut)]
             fn access(archetype: &Archetype) -> Option<Access> {
                 let mut access = Access::Iterate;
@@ -602,18 +622,19 @@ macro_rules! tuple_impl {
                 $($name::borrow(archetype);)*
             }
             #[allow(unused_variables)]
-            unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
-                Some(($($name::get(archetype, offset)?,)*))
+            fn new(archetype: &'a Archetype) -> Option<Self> {
+                Some(($($name::new(archetype)?,)*))
             }
             #[allow(unused_variables)]
             fn release(archetype: &Archetype) {
                 $($name::release(archetype);)*
             }
 
-            unsafe fn next(&mut self) -> Self::Item {
+            #[allow(unused_variables)]
+            unsafe fn get(&self, n: usize) -> Self::Item {
                 #[allow(non_snake_case)]
                 let ($($name,)*) = self;
-                ($($name.next(),)*)
+                ($($name.get(n),)*)
             }
         }
 
diff --git a/src/query_one.rs b/src/query_one.rs
index 3e609a3..781c8f6 100644
--- a/src/query_one.rs
+++ b/src/query_one.rs
@@ -37,10 +37,10 @@ impl<'a, Q: Query> QueryOne<'a, Q> {
             panic!("called QueryOnce::get twice; construct a new query instead");
         }
         unsafe {
-            let mut fetch = Q::Fetch::get(self.archetype, self.index as usize)?;
+            let fetch = Q::Fetch::new(self.archetype)?;
             self.borrowed = true;
             Q::Fetch::borrow(self.archetype);
-            Some(fetch.next())
+            Some(fetch.get(self.index as usize))
         }
     }
 
diff --git a/src/world.rs b/src/world.rs
index 3ff5b85..b212a7a 100644
--- a/src/world.rs
+++ b/src/world.rs
@@ -22,6 +22,7 @@ use std::error::Error;
 
 use hashbrown::{HashMap, HashSet};
 
+use crate::alloc::boxed::Box;
 use crate::archetype::Archetype;
 use crate::entities::{Entities, Location, ReserveEntitiesIterator};
 use crate::{
@@ -36,9 +37,22 @@ use crate::{
 ///
 /// The components of entities who have the same set of component types are stored in contiguous
 /// runs, allowing for extremely fast, cache-friendly iteration.
+///
+/// There is a maximum number of unique entity IDs, which means that there is a maximum number of live
+/// entities. When old entities are despawned, their IDs will be reused on a future entity, and
+/// old `Entity` values with that ID will be invalidated.
+///
+/// While using a world, spawning and despawning billions of entities may cause a rare `Entity`
+/// value collision. A newly spawned entity after this point may have an identical complete
+/// 'Entity' value as a previously despawned entity. If an extremly long-lived `Entity` value
+/// that referred to that previously despawned entity is still around by the time this happens,
+/// that long-lived `Entity` value will erroneously refer to the newly spawned entity, rather
+/// than being considered a reference to a dead entity. This is a very rare issue as an
+/// `Entity::id` needs to be reused over a billion times AND a copy of an `Entity` value with the
+/// same `Entity::id` needs to be kept around until after a new entity is spawned with that `Entity::id`.
 pub struct World {
     entities: Entities,
-    index: HashMap<Vec<TypeId>, u32>,
+    index: HashMap<Box<[TypeId]>, u32>,
     archetypes: Vec<Archetype>,
     archetype_generation: u64,
 }
@@ -50,7 +64,7 @@ impl World {
         let mut archetypes = Vec::new();
         archetypes.push(Archetype::new(Vec::new()));
         let mut index = HashMap::default();
-        index.insert(Vec::new(), 0);
+        index.insert(Box::default(), 0);
         Self {
             entities: Entities::default(),
             index,
@@ -83,11 +97,56 @@ impl World {
         self.flush();
 
         let entity = self.entities.alloc();
+
+        self.spawn_inner(entity, components);
+
+        entity
+    }
+
+    /// Create an entity with certain components and a specific `Entity` value.
+    ///
+    /// See `spawn`.
+    ///
+    /// Despawns any existing entity with the same `Entity::id`.
+    ///
+    /// Be cautious resurrecting old `Entity` values as it vastly increases
+    /// the likelihood of `Entity` value collisions ocurring (described above).
+    ///
+    /// # Example
+    /// ```
+    /// # use hecs::*;
+    /// let mut world = World::new();
+    /// let a = world.spawn((123, "abc"));
+    /// let b = world.spawn((456, true));
+    /// world.despawn(a);
+    /// assert!(!world.contains(a));
+    /// // all previous Entity values pointing to 'a' will be live again, instead pointing to the new entity.
+    /// world.spawn_at(a, (789, "ABC"));
+    /// assert!(world.contains(a));
+    /// ```
+    pub fn spawn_at(&mut self, entity: Entity, components: impl DynamicBundle) {
+        // Ensure all entity allocations are accounted for so `self.entities` can realloc if
+        // necessary
+        self.flush();
+
+        let loc = self.entities.alloc_at(entity);
+        if let Some(loc) = loc {
+            if let Some(moved) =
+                unsafe { self.archetypes[loc.archetype as usize].remove(loc.index) }
+            {
+                self.entities.meta[moved as usize].location.index = loc.index;
+            }
+        }
+
+        self.spawn_inner(entity, components);
+    }
+
+    fn spawn_inner(&mut self, entity: Entity, components: impl DynamicBundle) {
         let archetype_id = components.with_ids(|ids| {
             self.index.get(ids).copied().unwrap_or_else(|| {
                 let x = self.archetypes.len() as u32;
                 self.archetypes.push(Archetype::new(components.type_info()));
-                self.index.insert(ids.to_vec(), x);
+                self.index.insert(ids.into(), x);
                 self.archetype_generation += 1;
                 x
             })
@@ -104,7 +163,6 @@ impl World {
                 index,
             };
         }
-        entity
     }
 
     /// Efficiently spawn a large number of entities with the same components
@@ -187,7 +245,7 @@ impl World {
             self.index.get(ids).copied().unwrap_or_else(|| {
                 let x = self.archetypes.len() as u32;
                 self.archetypes.push(Archetype::new(T::static_type_info()));
-                self.index.insert(ids.to_vec(), x);
+                self.index.insert(ids.into(), x);
                 self.archetype_generation += 1;
                 x
             })
@@ -379,7 +437,7 @@ impl World {
             info.sort();
 
             // Find the archetype it'll live in
-            let elements = info.iter().map(|x| x.id()).collect::<Vec<_>>();
+            let elements = info.iter().map(|x| x.id()).collect();
             let target = match self.index.entry(elements) {
                 Entry::Occupied(x) => *x.get(),
                 Entry::Vacant(x) => {
@@ -464,7 +522,7 @@ impl World {
                 .cloned()
                 .filter(|x| !removed.contains(&x.id()))
                 .collect::<Vec<_>>();
-            let elements = info.iter().map(|x| x.id()).collect::<Vec<_>>();
+            let elements = info.iter().map(|x| x.id()).collect();
             let target = match self.index.entry(elements) {
                 Entry::Occupied(x) => *x.get(),
                 Entry::Vacant(x) => {
@@ -812,6 +870,21 @@ mod tests {
         assert_ne!(a.generation, b.generation);
     }
 
+    #[test]
+    fn spawn_at() {
+        let mut world = World::new();
+        let a = world.spawn(());
+        world.despawn(a).unwrap();
+        let b = world.spawn(());
+        assert!(world.contains(b));
+        assert_eq!(a.id, b.id);
+        assert_ne!(a.generation, b.generation);
+        world.spawn_at(a, ());
+        assert!(!world.contains(b));
+        assert_eq!(b.id, a.id);
+        assert_ne!(b.generation, a.generation);
+    }
+
     #[test]
     fn reuse_populated() {
         let mut world = World::new();


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
new file mode 100644
index 0000000..ca570ce
--- /dev/null
+++ b/tests/derive.rs
@@ -0,0 +1,23 @@
+//!
+
+#[cfg(feature = "macros")]
+#[cfg_attr(miri, ignore)]
+#[test]
+fn derive() {
+    const TEST_DIR: &str = "tests/derive";
+    let t = trybuild::TestCases::new();
+    let failures = &["enum.rs", "union.rs"];
+    let successes = &[
+        "unit_structs.rs",
+        "tuple_structs.rs",
+        "named_structs.rs",
+        "no_prelude.rs",
+        "generics.rs",
+    ];
+    for &passing_test in successes {
+        t.pass(&format!("{}/{}", TEST_DIR, passing_test));
+    }
+    for &failing_test in failures {
+        t.compile_fail(&format!("{}/{}", TEST_DIR, failing_test));
+    }
+}
diff --git a/tests/derive/enum.rs b/tests/derive/enum.rs
new file mode 100644
index 0000000..569f1d7
--- /dev/null
+++ b/tests/derive/enum.rs
@@ -0,0 +1,6 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+enum Foo {}
+
+fn main() {}
diff --git a/tests/derive/enum.stderr b/tests/derive/enum.stderr
new file mode 100644
index 0000000..bec72c6
--- /dev/null
+++ b/tests/derive/enum.stderr
@@ -0,0 +1,5 @@
+error: derive(Bundle) does not support enums or unions
+ --> $DIR/enum.rs:4:6
+  |
+4 | enum Foo {}
+  |      ^^^
diff --git a/tests/derive/generics.rs b/tests/derive/generics.rs
new file mode 100644
index 0000000..f15eccb
--- /dev/null
+++ b/tests/derive/generics.rs
@@ -0,0 +1,21 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+struct Foo<T> {
+    foo: T,
+}
+
+#[derive(Bundle)]
+struct Bar<T> {
+    foo: i32,
+    bar: T,
+}
+
+#[derive(Bundle)]
+struct Baz<T, U, V> {
+    foo: T,
+    bar: U,
+    baz: V,
+}
+
+fn main() {}
diff --git a/tests/derive/named_structs.rs b/tests/derive/named_structs.rs
new file mode 100644
index 0000000..9ef9b26
--- /dev/null
+++ b/tests/derive/named_structs.rs
@@ -0,0 +1,21 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+struct Foo {
+    foo: i32,
+}
+
+#[derive(Bundle)]
+struct Bar {
+    foo: i32,
+    bar: String,
+}
+
+#[derive(Bundle)]
+struct Baz {
+    foo: i32,
+    bar: String,
+    baz: &'static str,
+}
+
+fn main() {}
diff --git a/tests/derive/no_prelude.rs b/tests/derive/no_prelude.rs
new file mode 100644
index 0000000..6d6fd38
--- /dev/null
+++ b/tests/derive/no_prelude.rs
@@ -0,0 +1,8 @@
+#![no_implicit_prelude]
+
+#[derive(::hecs::Bundle)]
+struct Foo {
+    foo: (),
+}
+
+fn main() {}
diff --git a/tests/derive/tuple_structs.rs b/tests/derive/tuple_structs.rs
new file mode 100644
index 0000000..68729b1
--- /dev/null
+++ b/tests/derive/tuple_structs.rs
@@ -0,0 +1,12 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+struct Foo(i32);
+
+#[derive(Bundle)]
+struct Bar(i32, String);
+
+#[derive(Bundle)]
+struct Baz(i32, String, &'static str);
+
+fn main() {}
diff --git a/tests/derive/union.rs b/tests/derive/union.rs
new file mode 100644
index 0000000..3c2f1d5
--- /dev/null
+++ b/tests/derive/union.rs
@@ -0,0 +1,8 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+union Foo {
+    u8: u8,
+}
+
+fn main() {}
diff --git a/tests/derive/union.stderr b/tests/derive/union.stderr
new file mode 100644
index 0000000..87ba7b0
--- /dev/null
+++ b/tests/derive/union.stderr
@@ -0,0 +1,5 @@
+error: derive(Bundle) does not support enums or unions
+ --> $DIR/union.rs:4:7
+  |
+4 | union Foo {
+  |       ^^^
diff --git a/tests/derive/unit_structs.rs b/tests/derive/unit_structs.rs
new file mode 100644
index 0000000..7dd6314
--- /dev/null
+++ b/tests/derive/unit_structs.rs
@@ -0,0 +1,12 @@
+use hecs::Bundle;
+
+#[derive(Bundle)]
+struct NamedUnit {}
+
+#[derive(Bundle)]
+struct TupleUnit();
+
+#[derive(Bundle)]
+struct Unit;
+
+fn main() {}
diff --git a/tests/tests.rs b/tests/tests.rs
index 2660155..b94f1d6 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -243,7 +243,18 @@ fn derived_bundle() {
 
 #[test]
 #[cfg(feature = "macros")]
-#[should_panic(expected = "each type must occur at most once")]
+#[cfg_attr(
+    debug_assertions,
+    should_panic(
+        expected = "attempted to allocate entity with duplicate i32 components; each type must occur at most once!"
+    )
+)]
+#[cfg_attr(
+    not(debug_assertions),
+    should_panic(
+        expected = "attempted to allocate entity with duplicate components; each type must occur at most once!"
+    )
+)]
 fn bad_bundle_derive() {
     #[derive(Bundle)]
     struct Foo {
@@ -368,3 +379,21 @@ fn query_one() {
     world.despawn(a).unwrap();
     assert!(world.query_one::<&i32>(a).is_err());
 }
+
+#[test]
+#[cfg_attr(
+    debug_assertions,
+    should_panic(
+        expected = "attempted to allocate entity with duplicate f32 components; each type must occur at most once!"
+    )
+)]
+#[cfg_attr(
+    not(debug_assertions),
+    should_panic(
+        expected = "attempted to allocate entity with duplicate components; each type must occur at most once!"
+    )
+)]
+fn duplicate_components_panic() {
+    let mut world = World::new();
+    world.reserve::<(f32, i64, f32)>(1);
+}