diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index 903785b5..cc6be56f 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -37,10 +37,7 @@ fn calculate_waiting_time(context: &Context, shape: f64, mean_period: f64) -> f6 } fn expose_network>(context: &mut Context, beta: f64) { - let mut infectious_people = Vec::new(); - context.with_query_results((DiseaseStatus::I,), &mut |infected| { - infectious_people = infected.iter().copied().collect(); - }); + let infectious_people = context.query((DiseaseStatus::I,)).to_owned_vec(); for infectious in infectious_people { let edges = context.get_matching_edges::(infectious, |context, edge| { @@ -191,9 +188,7 @@ mod tests { let mut to_infect = Vec::::new(); context.with_query_results((Id(71),), &mut |people| { - for p in people { - to_infect.push(*p); - } + to_infect.extend(people); }); init(&mut context, &to_infect); diff --git a/ixa-bench/criterion/index_benches.rs b/ixa-bench/criterion/index_benches.rs index 43777940..74caf411 100644 --- a/ixa-bench/criterion/index_benches.rs +++ b/ixa-bench/criterion/index_benches.rs @@ -46,7 +46,7 @@ pub fn criterion_benchmark(criterion: &mut Criterion) { context.with_query_results( black_box((Property10(number % 10),)), &mut |entity_ids| { - black_box(entity_ids.len()); + black_box(entity_ids.len_upper()); }, ); } @@ -65,7 +65,7 @@ pub fn criterion_benchmark(criterion: &mut Criterion) { Property100(*number), )), &mut |entity_ids| { - black_box(entity_ids.len()); + black_box(entity_ids.len_upper()); }, ); } @@ -85,7 +85,7 @@ pub fn criterion_benchmark(criterion: &mut Criterion) { MultiProperty100(*number), )), &mut |entity_ids| { - black_box(entity_ids.len()); + black_box(entity_ids.len_upper()); }, ); } diff --git a/src/entity/context_extension.rs b/src/entity/context_extension.rs index 4f02623c..61d2cc94 100644 --- a/src/entity/context_extension.rs +++ b/src/entity/context_extension.rs @@ -1,14 +1,14 @@ use std::any::{Any, TypeId}; -use crate::entity::entity_set::EntitySetIterator; +use crate::entity::entity_set::{EntitySet, EntitySetIterator, SourceSet}; use crate::entity::events::{EntityCreatedEvent, PartialPropertyChangeEvent}; use crate::entity::index::{IndexCountResult, IndexSetResult, PropertyIndexType}; use crate::entity::property::Property; use crate::entity::property_list::PropertyList; use crate::entity::query::Query; use crate::entity::{Entity, EntityId, PopulationIterator}; -use crate::hashing::IndexSet; use crate::rand::Rng; +use crate::random::sample_multiple_from_known_length; use crate::{warn, Context, ContextRandomExt, IxaError, RngId}; /// A trait extension for [`Context`] that exposes entity-related @@ -59,13 +59,13 @@ pub trait ContextEntitiesExt { #[cfg(test)] fn is_property_indexed>(&self) -> bool; - /// This method gives client code direct immutable access to the fully realized set of - /// entity IDs. This is especially efficient for indexed queries, as this method reduces - /// to a simple lookup of a hash bucket. Otherwise, the set is allocated and computed. - fn with_query_results>( - &self, + /// This method gives client code direct access to the query result as an `EntitySet`. + /// This is especially efficient for indexed queries, as this method can reduce to wrapping + /// a single indexed source. + fn with_query_results<'a, E: Entity, Q: Query>( + &'a self, query: Q, - callback: &mut dyn FnMut(&IndexSet>), + callback: &mut dyn FnMut(EntitySet<'a, E>), ); /// Gives the count of distinct entity IDs satisfying the query. This is especially @@ -103,6 +103,9 @@ pub trait ContextEntitiesExt { /// Returns an iterator over all created entities of type `E`. fn get_entity_iterator(&self) -> PopulationIterator; + /// Generates an `EntitySet` representing the query results. + fn query>(&self, query: Q) -> EntitySet; + /// Generates an iterator over the results of the query. fn query_result_iterator>(&self, query: Q) -> EntitySetIterator; @@ -239,14 +242,14 @@ impl ContextEntitiesExt for Context { property_store.is_property_indexed::

() } - fn with_query_results>( - &self, + fn with_query_results<'a, E: Entity, Q: Query>( + &'a self, query: Q, - callback: &mut dyn FnMut(&IndexSet>), + callback: &mut dyn FnMut(EntitySet<'a, E>), ) { // The fast path for indexed queries. - // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `Query:: new_query_result_iterator`. + // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `Query::new_query_result`. // The difference is, we access the index set if we find it. if let Some(multi_property_id) = query.multi_property_id() { let property_store = self.entity_store.get_property_store::(); @@ -256,12 +259,11 @@ impl ContextEntitiesExt for Context { query.multi_property_value_hash(), ) { IndexSetResult::Set(people_set) => { - callback(&people_set); + callback(EntitySet::from_source(SourceSet::IndexSet(people_set))); return; } IndexSetResult::Empty => { - let people_set = IndexSet::default(); - callback(&people_set); + callback(EntitySet::empty()); return; } IndexSetResult::Unsupported => {} @@ -272,25 +274,23 @@ impl ContextEntitiesExt for Context { // Special case the empty query, which creates a set containing the entire population. if query.type_id() == TypeId::of::<()>() { warn!("Called Context::with_query_results() with an empty query. Prefer Context::get_entity_iterator::() for working with the entire population."); - let entity_set = self.get_entity_iterator::().collect::>(); - callback(&entity_set); + callback(EntitySet::from_source(SourceSet::Population( + self.get_entity_count::(), + ))); return; } // The slow path of computing the full query set. warn!("Called Context::with_query_results() with an unindexed query. It's almost always better to use Context::query_result_iterator() for unindexed queries."); - // Fall back to `EntitySetIterator`. - let people_set = query - .new_query_result_iterator(self) - .collect::>(); - callback(&people_set); + // Fall back to the query's `EntitySet`. + callback(self.query(query)); } fn query_entity_count>(&self, query: Q) -> usize { // The fast path for indexed queries. // - // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `Query:: new_query_result_iterator`. + // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `Query::new_query_result`. if let Some(multi_property_id) = query.multi_property_id() { let property_store = self.entity_store.get_property_store::(); match property_store.get_index_count_with_hash_for_property_id( @@ -313,6 +313,22 @@ impl ContextEntitiesExt for Context { R: RngId + 'static, R::RngType: Rng, { + if query.type_id() == TypeId::of::<()>() { + let population = self.get_entity_count::(); + return self.sample(rng_id, move |rng| { + if population == 0 { + warn!("Requested a sample entity from an empty population"); + return None; + } + let index = if population <= u32::MAX as usize { + rng.random_range(0..population as u32) as usize + } else { + rng.random_range(0..population) + }; + Some(EntityId::new(index)) + }); + } + let query_result = self.query_result_iterator(query); self.sample(rng_id, move |rng| query_result.sample_entity(rng)) } @@ -324,6 +340,20 @@ impl ContextEntitiesExt for Context { R: RngId + 'static, R::RngType: Rng, { + if query.type_id() == TypeId::of::<()>() { + let population = self.get_entity_count::(); + return self.sample(rng_id, move |rng| { + if population == 0 { + warn!("Requested a sample of entities from an empty population"); + return vec![]; + } + if n >= population { + return PopulationIterator::::new(population).collect(); + } + sample_multiple_from_known_length(rng, PopulationIterator::::new(population), n) + }); + } + let query_result = self.query_result_iterator(query); self.sample(rng_id, move |rng| query_result.sample_entities(rng, n)) } @@ -336,6 +366,10 @@ impl ContextEntitiesExt for Context { self.entity_store.get_entity_iterator::() } + fn query>(&self, query: Q) -> EntitySet { + query.new_query_result(self) + } + fn query_result_iterator>(&self, query: Q) -> EntitySetIterator { query.new_query_result_iterator(self) } @@ -524,13 +558,13 @@ mod tests { let mut existing_len = 0; context.with_query_results((existing_value,), &mut |people_set| { - existing_len = people_set.len(); + existing_len = people_set.into_iter().count(); }); assert_eq!(existing_len, 2); let mut missing_len = 0; context.with_query_results((missing_value,), &mut |people_set| { - missing_len = people_set.len(); + missing_len = people_set.into_iter().count(); }); assert_eq!(missing_len, 0); @@ -786,12 +820,12 @@ mod tests { // Force an index build by running a query. let _ = context.query_result_iterator((InfectionStatus::Susceptible, Vaccinated(true))); - // Capture the address of the has set given by `with_query_result` - let mut address: *const IndexSet> = std::ptr::null(); + // Capture the set given by `with_query_results`. + let mut result_entities: IndexSet> = IndexSet::default(); context.with_query_results( (InfectionStatus::Susceptible, Vaccinated(true)), &mut |result_set| { - address = result_set as *const _; + result_entities = result_set.into_iter().collect::>(); }, ); @@ -818,8 +852,37 @@ mod tests { ) .unwrap(); - let address2 = &*bucket as *const _; - assert_eq!(address2, address); + let expected_entities = bucket.iter().copied().collect::>(); + assert_eq!(expected_entities, result_entities); + } + + #[test] + fn query_returns_entity_set_and_query_result_iterator_remains_compatible() { + let mut context = Context::new(); + let p1 = context + .add_entity((Age(21), InfectionStatus::Susceptible, Vaccinated(true))) + .unwrap(); + let _p2 = context + .add_entity((Age(22), InfectionStatus::Susceptible, Vaccinated(false))) + .unwrap(); + let p3 = context + .add_entity((Age(23), InfectionStatus::Infected, Vaccinated(true))) + .unwrap(); + + let query = (Vaccinated(true),); + + let from_set = context + .query::(query) + .into_iter() + .collect::>(); + let from_iterator = context + .query_result_iterator(query) + .collect::>(); + + assert_eq!(from_set, from_iterator); + assert!(from_set.contains(&p1)); + assert!(from_set.contains(&p3)); + assert_eq!(from_set.len(), 2); } #[test] diff --git a/src/entity/entity.rs b/src/entity/entity.rs index f027a164..69c42e43 100644 --- a/src/entity/entity.rs +++ b/src/entity/entity.rs @@ -146,6 +146,12 @@ impl PopulationIterator { _phantom: PhantomData, } } + + #[must_use] + #[allow(dead_code)] + pub(crate) fn population(&self) -> usize { + self.population + } } impl Iterator for PopulationIterator { diff --git a/src/entity/entity_set/entity_set.rs b/src/entity/entity_set/entity_set.rs new file mode 100644 index 00000000..8580f5a1 --- /dev/null +++ b/src/entity/entity_set/entity_set.rs @@ -0,0 +1,482 @@ +//! A lazy, composable set type built from set-algebraic expressions. +//! +//! An [`EntitySet`] represents a set of [`EntityId`] values as a tree of union, +//! intersection, and difference operations over leaf [`SourceSet`] nodes. The tree +//! is constructed eagerly but evaluated lazily: membership tests ([`contains`]) walk +//! the tree on demand, and iteration is deferred to [`EntitySetIterator`]. +//! +//! Construction methods apply algebraic simplifications (e.g. `A ∪ ∅ = A`, +//! `A ∩ A = A`) and reorder operands by estimated size to improve short-circuit +//! performance. +//! +//! [`contains`]: EntitySet::contains + +use super::{EntitySetIterator, SourceSet}; +use crate::entity::{Entity, EntityId}; + +/// Opaque public wrapper around the internal set-expression tree. +pub struct EntitySet<'a, E: Entity>(EntitySetInner<'a, E>); + +/// Internal set-expression tree used to represent composed query sources. +pub(super) enum EntitySetInner<'a, E: Entity> { + Source(SourceSet<'a, E>), + Union(Box>, Box>), + Intersection(Vec>), + Difference(Box>, Box>), +} + +impl<'a, E: Entity> Default for EntitySet<'a, E> { + fn default() -> Self { + Self::empty() + } +} + +impl<'a, E: Entity> EntitySet<'a, E> { + pub(super) fn into_inner(self) -> EntitySetInner<'a, E> { + self.0 + } + + pub(super) fn is_source_leaf(&self) -> bool { + matches!(self.0, EntitySetInner::Source(_)) + } + + pub(super) fn into_source_leaf(self) -> Option> { + match self.0 { + EntitySetInner::Source(source) => Some(source), + _ => None, + } + } + + /// Create an empty entity set. + pub fn empty() -> Self { + EntitySet(EntitySetInner::Source(SourceSet::Empty)) + } + + /// Create an entity set from a single source set. + pub(crate) fn from_source(source: SourceSet<'a, E>) -> Self { + EntitySet(EntitySetInner::Source(source)) + } + + pub(crate) fn from_intersection_sources(mut sources: Vec>) -> Self { + match sources.len() { + 0 => return Self::empty(), + 1 => return Self::from_source(sources.pop().unwrap()), + _ => {} + } + + // Keep intersections sorted smallest-to-largest so iterators can take the + // first source as the driver and membership checks short-circuit quickly. + sources.sort_unstable_by_key(SourceSet::len_upper); + + let sets = sources.into_iter().map(Self::from_source).collect(); + + EntitySet(EntitySetInner::Intersection(sets)) + } + + pub fn union(self, other: Self) -> Self { + // Identity: A ∪ ∅ = A, ∅ ∪ B = B + if self.is_empty() { + return other; + } + if other.is_empty() { + return self; + } + // Idempotence: A ∪ A = A (same structure over same sources) + if self.structurally_eq(&other) { + return self; + } + // Universal absorption: U ∪ A = U, A ∪ U = U + if self.is_universal() { + return self; + } + if other.is_universal() { + return other; + } + // Singleton absorption: {e} ∪ A = A if e ∈ A + if let Some(e) = self.as_singleton() { + if other.contains(e) { + return other; + } + } + if let Some(e) = other.as_singleton() { + if self.contains(e) { + return self; + } + } + + // Larger set on LHS: more likely to short-circuit `||`. + let (left, right) = if self.len_upper() >= other.len_upper() { + (self, other) + } else { + (other, self) + }; + EntitySet(EntitySetInner::Union(Box::new(left), Box::new(right))) + } + + pub fn intersection(self, other: Self) -> Self { + // Annihilator: A ∩ ∅ = ∅ + if self.is_empty() || other.is_empty() { + return Self::empty(); + } + // Idempotence: A ∩ A = A + if self.structurally_eq(&other) { + return self; + } + // Identity: U ∩ A = A + if self.is_universal() { + return other; + } + if other.is_universal() { + return self; + } + // Singleton restriction: + // {e} ∩ A = {e} if e ∈ A, otherwise ∅ + if let Some(e) = self.as_singleton() { + return if other.contains(e) { + self + } else { + Self::empty() + }; + } + if let Some(e) = other.as_singleton() { + return if self.contains(e) { + other + } else { + Self::empty() + }; + } + + let mut sets = match self { + EntitySet(EntitySetInner::Intersection(sets)) => sets, + _ => vec![self], + }; + + sets.push(other); + // Keep intersections sorted smallest-to-largest so iterators can take the + // first source as the driver and membership checks short-circuit quickly. + sets.sort_unstable_by_key(|a| a.len_upper()); + EntitySet(EntitySetInner::Intersection(sets)) + } + + pub fn difference(self, other: Self) -> Self { + // Identity: A \ ∅ = A + if other.is_empty() { + return self; + } + // Annihilator: ∅ \ B = ∅ + if self.is_empty() { + return Self::empty(); + } + // Self-subtraction: A \ A = ∅ + if self.structurally_eq(&other) { + return Self::empty(); + } + // Universal subtraction: A \ U = ∅ + if other.is_universal() { + return Self::empty(); + } + // Singleton restriction: + // {e} \ A = {e} if e ∉ A, otherwise ∅ + if let Some(e) = self.as_singleton() { + return if other.contains(e) { + Self::empty() + } else { + self + }; + } + EntitySet(EntitySetInner::Difference(Box::new(self), Box::new(other))) + } + + /// Test whether `entity_id` is a member of this set. + pub fn contains(&self, entity_id: EntityId) -> bool { + match self { + EntitySet(EntitySetInner::Source(source)) => source.contains(entity_id), + EntitySet(EntitySetInner::Union(a, b)) => { + a.contains(entity_id) || b.contains(entity_id) + } + EntitySet(EntitySetInner::Intersection(sets)) => { + sets.iter().all(|set| set.contains(entity_id)) + } + EntitySet(EntitySetInner::Difference(a, b)) => { + a.contains(entity_id) && !b.contains(entity_id) + } + } + } + + /// Upper bound on the number of entities in this set. + pub fn len_upper(&self) -> usize { + match self { + EntitySet(EntitySetInner::Source(source)) => source.len_upper(), + EntitySet(EntitySetInner::Union(a, b)) => a.len_upper().saturating_add(b.len_upper()), + EntitySet(EntitySetInner::Intersection(sets)) => { + sets.iter().map(EntitySet::len_upper).min().unwrap_or(0) + } + EntitySet(EntitySetInner::Difference(a, _)) => a.len_upper(), + } + } + + /// Collect this set's contents into an owned vector of `EntityId`. + pub fn to_owned_vec(self) -> Vec> { + self.into_iter().collect() + } + + /// Returns `true` if this set is the abstract empty set (`∅`). + /// + /// A return value of `false` does not guarantee the set is non-empty. + /// For example, it may be an intersection of disjoint sets. + fn is_empty(&self) -> bool { + matches!(self, EntitySet(EntitySetInner::Source(SourceSet::Empty))) + } + /// Returns `true` if this set represents the entire entity population. + fn is_universal(&self) -> bool { + matches!( + self, + EntitySet(EntitySetInner::Source(SourceSet::Population(_))) + ) + } + /// Returns the contained entity id if this set is a singleton leaf. + fn as_singleton(&self) -> Option> { + match self { + EntitySet(EntitySetInner::Source(SourceSet::Entity(e))) => Some(*e), + _ => None, + } + } + + /// Structural equality check: same tree shape with same sources at leaves. + fn structurally_eq(&self, other: &Self) -> bool { + match (self, other) { + (EntitySet(EntitySetInner::Source(a)), EntitySet(EntitySetInner::Source(b))) => a == b, + ( + EntitySet(EntitySetInner::Union(a1, a2)), + EntitySet(EntitySetInner::Union(b1, b2)), + ) + | ( + EntitySet(EntitySetInner::Difference(a1, a2)), + EntitySet(EntitySetInner::Difference(b1, b2)), + ) => a1.structurally_eq(b1) && a2.structurally_eq(b2), + ( + EntitySet(EntitySetInner::Intersection(a_sets)), + EntitySet(EntitySetInner::Intersection(b_sets)), + ) => { + a_sets.len() == b_sets.len() + && a_sets + .iter() + .zip(b_sets.iter()) + .all(|(a_set, b_set)| a_set.structurally_eq(b_set)) + } + _ => false, + } + } +} + +impl<'a, E: Entity> IntoIterator for EntitySet<'a, E> { + type Item = EntityId; + type IntoIter = EntitySetIterator<'a, E>; + + fn into_iter(self) -> Self::IntoIter { + EntitySetIterator::new(self) + } +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + + use super::*; + use crate::define_entity; + use crate::hashing::IndexSet; + + define_entity!(Person); + + fn finite_set(ids: &[usize]) -> RefCell>> { + RefCell::new( + ids.iter() + .copied() + .map(EntityId::::new) + .collect::>(), + ) + } + + fn as_entity_set(set: &RefCell>>) -> EntitySet { + EntitySet::from_source(SourceSet::IndexSet(set.borrow())) + } + + #[test] + fn from_source_empty_is_empty() { + let es = EntitySet::::from_source(SourceSet::Empty); + assert_eq!(es.len_upper(), 0); + for value in 0..10 { + assert!(!es.contains(EntityId::::new(value))); + } + } + + #[test] + fn from_source_entity_and_population() { + let entity = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(5))); + assert!(entity.contains(EntityId::::new(5))); + assert!(!entity.contains(EntityId::::new(4))); + assert_eq!(entity.len_upper(), 1); + + let population = EntitySet::from_source(SourceSet::::Population(3)); + assert!(population.contains(EntityId::::new(0))); + assert!(population.contains(EntityId::::new(2))); + assert!(!population.contains(EntityId::::new(3))); + assert_eq!(population.len_upper(), 3); + } + + #[test] + fn union_algebraic_reductions() { + let a = finite_set(&[1, 2, 3]); + let e = EntitySet::::empty(); + let u = EntitySet::from_source(SourceSet::::Population(10)); + + let a_union_empty = as_entity_set(&a).union(e); + assert!(a_union_empty.contains(EntityId::::new(1))); + assert!(!a_union_empty.contains(EntityId::::new(4))); + + let u_union_a = u.union(as_entity_set(&a)); + assert!(matches!( + u_union_a, + EntitySet(EntitySetInner::Source(SourceSet::Population(10))) + )); + } + + #[test] + fn union_entity_absorption() { + let a = finite_set(&[1, 2, 3]); + let absorbed = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(2))) + .union(as_entity_set(&a)); + assert!(absorbed.contains(EntityId::::new(1))); + assert!(absorbed.contains(EntityId::::new(2))); + assert!(absorbed.contains(EntityId::::new(3))); + + let b = finite_set(&[1, 2, 3]); + let not_absorbed = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(8))) + .union(as_entity_set(&b)); + assert!(not_absorbed.contains(EntityId::::new(8))); + assert!(not_absorbed.contains(EntityId::::new(1))); + } + + #[test] + fn intersection_algebraic_reductions() { + let a = finite_set(&[1, 2, 3]); + let u = EntitySet::from_source(SourceSet::::Population(10)); + + let a_inter_u = as_entity_set(&a).intersection(u); + assert!(a_inter_u.contains(EntityId::::new(1))); + assert!(a_inter_u.contains(EntityId::::new(2))); + assert!(!a_inter_u.contains(EntityId::::new(9))); + + let b = finite_set(&[1, 2, 3]); + let present = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(2))) + .intersection(as_entity_set(&b)); + assert!(matches!( + present, + EntitySet(EntitySetInner::Source(SourceSet::Entity(_))) + )); + + let c = finite_set(&[1, 2, 3]); + let absent = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(7))) + .intersection(as_entity_set(&c)); + assert!(!absent.contains(EntityId::::new(7))); + } + + #[test] + fn difference_algebraic_reductions() { + let a = finite_set(&[1, 2, 3]); + + let minus_empty = as_entity_set(&a).difference(EntitySet::empty()); + assert!(minus_empty.contains(EntityId::::new(1))); + assert!(!minus_empty.contains(EntityId::::new(9))); + + let minus_universe = + as_entity_set(&a) + .difference(EntitySet::from_source(SourceSet::::Population(10))); + for value in 0..10 { + assert!(!minus_universe.contains(EntityId::::new(value))); + } + + let b = finite_set(&[1, 2, 3]); + let singleton_absent = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(8))) + .difference(as_entity_set(&b)); + assert!(singleton_absent.contains(EntityId::::new(8))); + + let c = finite_set(&[1, 2, 3]); + let singleton_present = + EntitySet::from_source(SourceSet::::Entity(EntityId::::new(2))) + .difference(as_entity_set(&c)); + assert!(!singleton_present.contains(EntityId::::new(2))); + } + + #[test] + fn difference_is_not_commutative() { + let a = finite_set(&[1, 2, 3]); + let b = finite_set(&[2, 3, 4]); + + let d1 = as_entity_set(&a).difference(as_entity_set(&b)); + let c = finite_set(&[2, 3, 4]); + let d = finite_set(&[1, 2, 3]); + let d2 = as_entity_set(&c).difference(as_entity_set(&d)); + + assert!(d1.contains(EntityId::::new(1))); + assert!(!d1.contains(EntityId::::new(4))); + assert!(d2.contains(EntityId::::new(4))); + assert!(!d2.contains(EntityId::::new(1))); + } + + #[test] + fn len_upper_rules() { + let a = finite_set(&[1, 2]); + let b = finite_set(&[2, 3, 4]); + + let union = as_entity_set(&a).union(as_entity_set(&b)); + assert_eq!(union.len_upper(), a.borrow().len() + b.borrow().len()); + + let intersection = as_entity_set(&a).intersection(as_entity_set(&b)); + assert_eq!( + intersection.len_upper(), + a.borrow().len().min(b.borrow().len()) + ); + + let difference = as_entity_set(&a).difference(as_entity_set(&b)); + assert_eq!(difference.len_upper(), a.borrow().len()); + } + + #[test] + fn compound_expressions_membership() { + let a = finite_set(&[1, 2, 3, 4]); + let b = finite_set(&[3, 4, 5]); + let c = finite_set(&[10, 20]); + let d = finite_set(&[20]); + + let union_of_intersections = as_entity_set(&a) + .intersection(as_entity_set(&b)) + .union(as_entity_set(&c).intersection(as_entity_set(&d))); + assert!(union_of_intersections.contains(EntityId::::new(3))); + assert!(union_of_intersections.contains(EntityId::::new(4))); + assert!(union_of_intersections.contains(EntityId::::new(20))); + assert!(!union_of_intersections.contains(EntityId::::new(5))); + + let a2 = finite_set(&[1, 2, 3]); + let b2 = finite_set(&[3, 4, 5]); + let a3 = finite_set(&[1, 2, 3]); + let law = as_entity_set(&a3).intersection(as_entity_set(&a2).union(as_entity_set(&b2))); + assert!(law.contains(EntityId::::new(1))); + assert!(law.contains(EntityId::::new(2))); + assert!(law.contains(EntityId::::new(3))); + assert!(!law.contains(EntityId::::new(4))); + } + + #[test] + fn population_zero_is_empty() { + let es = EntitySet::from_source(SourceSet::::Population(0)); + assert_eq!(es.len_upper(), 0); + assert!(!es.contains(EntityId::::new(0))); + } +} diff --git a/src/entity/entity_set/entity_set_iterator.rs b/src/entity/entity_set/entity_set_iterator.rs index 7b745b88..5eff20d0 100644 --- a/src/entity/entity_set/entity_set_iterator.rs +++ b/src/entity/entity_set/entity_set_iterator.rs @@ -1,76 +1,273 @@ -//! An `EntitySetIterator` encapsulates the execution of an entity set operation, presenting the -//! results as an iterator. +//! Iterator implementation for [`EntitySet`]. //! -//! You normally create an `EntitySetIterator` by calling `context.query_entity(some_query)`, -//! which returns an `EntitySetIterator` instance. +//! `EntitySetIterator` mirrors an `EntitySet` expression tree and evaluates nodes lazily. +//! - `Source` iterates a concrete backing source (`Population`, singleton `Entity`, index set, +//! or property-backed source). +//! - `Intersection` and `Difference` drive iteration from one branch and filter candidates using +//! membership checks. +//! - `Union` yields the left branch first, then lazily activates the right branch (`Pending` to +//! `Active`) and filters out any IDs already present in the left branch via membership checks. //! -//! An `EntitySetIterator` can be thought of abstractly as representing the intersection of -//! a set of `SourceSet`s. Internally, `EntitySetIterator` holds its state in a set of -//! `SourceSet` instances and a `SourceIterator`, which is an iterator created from a -//! `SourceSet` and represents the first of the sets in the intersection. To produce the "next" -//! `EntityId` for a call to `EntitySetIterator::next()`, we need to iterate over the -//! `SourceIterator` until we find an entity ID that is contained in all of the `SourceSet`s -//! simultaneously. -//! -//! An `EntitySetIterator` holds an immutable reference to the `Context`, so -//! operations that mutate `Context` will be forbidden by the compiler statically. The results -//! can be collected into a `Vec` (or other container) with the `collect` idiom for use cases -//! where you want a mutable copy of the result set. If you don't need a mutable copy, use -//! `Context::with_query_results` instead, as it is much more efficient for indexed queries. +//! The iterator is created through `EntitySet::into_iter()`. use std::cell::Ref; use log::warn; use rand::Rng; -use crate::entity::entity_set::source_set::{SourceIterator, SourceSet}; +use crate::entity::entity_set::entity_set::{EntitySet, EntitySetInner}; +use crate::entity::entity_set::source_iterator::SourceIterator; +use crate::entity::entity_set::source_set::SourceSet; use crate::entity::{Entity, EntityId, PopulationIterator}; use crate::hashing::IndexSet; use crate::random::{ sample_multiple_from_known_length, sample_multiple_l_reservoir, sample_single_l_reservoir, }; +enum EntitySetIteratorInner<'a, E: Entity> { + Empty, + Source(SourceIterator<'a, E>), + // The `IntersectionSources` variant is a micro-optimization to avoid recursive + // `EntitySet` membership checks in the most common case, improving tight-loop + // benchmark performance by 5%-15%. + IntersectionSources { + driver: SourceIterator<'a, E>, + filters: Vec>, + }, + Intersection { + driver: Box>, + filters: Vec>, + }, + Difference { + left: Box>, + right: EntitySet<'a, E>, + }, + Union { + left: Box>, + right: UnionRightState<'a, E>, + }, +} + +enum UnionRightState<'a, E: Entity> { + Pending(Option>), + Active(Box>), +} + +impl<'a, E: Entity> EntitySetIteratorInner<'a, E> { + fn from_entity_set(set: EntitySet<'a, E>) -> Self { + match set.into_inner() { + EntitySetInner::Source(source) => { + if matches!(source, SourceSet::Empty) { + Self::Empty + } else { + Self::Source(source.into_iter()) + } + } + EntitySetInner::Intersection(mut sets) => { + if sets.is_empty() { + return Self::Empty; + } + if sets.len() == 1 { + return Self::from_entity_set(sets.pop().unwrap()); + } + + if sets.iter().all(EntitySet::is_source_leaf) { + // `EntitySet::Intersection` stores operands sorted ascending by `len_upper`. + // Keep that order so membership checks short-circuit early on small filters. + let mut set_iter = sets.into_iter(); + + let first = set_iter.next().unwrap().into_source_leaf().unwrap(); + let driver = first.into_iter(); + + let filters = set_iter + .map(|set| set.into_source_leaf().unwrap()) + .collect(); + + return Self::IntersectionSources { driver, filters }; + } + + // `EntitySet::Intersection` stores operands sorted ascending by `len_upper`. + // Use the first set as the iteration driver and keep the remaining filters in + // ascending order for short-circuit-friendly `contains` checks. + let mut set_iter = sets.into_iter(); + let driver = Box::new(Self::from_entity_set(set_iter.next().unwrap())); + Self::Intersection { + driver, + filters: set_iter.collect(), + } + } + EntitySetInner::Difference(left, right) => Self::Difference { + left: Box::new(Self::from_entity_set(*left)), + right: *right, + }, + EntitySetInner::Union(left, right) => Self::Union { + left: Box::new(Self::from_entity_set(*left)), + right: UnionRightState::Pending(Some(*right)), + }, + } + } + + fn contains(&self, entity_id: EntityId) -> bool { + match self { + Self::Empty => false, + Self::Source(iter) => iter.contains(entity_id), + Self::IntersectionSources { driver, filters } => { + driver.contains(entity_id) && filters.iter().all(|set| set.contains(entity_id)) + } + Self::Intersection { driver, filters } => { + driver.contains(entity_id) && filters.iter().all(|set| set.contains(entity_id)) + } + Self::Difference { left, right } => { + left.contains(entity_id) && !right.contains(entity_id) + } + Self::Union { left, right } => left.contains(entity_id) || right.contains(entity_id), + } + } +} + +impl<'a, E: Entity> UnionRightState<'a, E> { + fn contains(&self, entity_id: EntityId) -> bool { + match self { + Self::Pending(Some(set)) => set.contains(entity_id), + Self::Pending(None) => false, + Self::Active(iter) => iter.contains(entity_id), + } + } +} + +impl<'a, E: Entity> EntitySetIteratorInner<'a, E> { + #[inline] + fn next_inner(&mut self) -> Option> { + match self { + Self::Empty => None, + Self::Source(source) => source.next(), + Self::IntersectionSources { driver, filters } => driver + .by_ref() + .find(|&entity_id| filters.iter().all(|filter| filter.contains(entity_id))), + Self::Intersection { driver, filters } => { + while let Some(entity_id) = driver.next_inner() { + if filters.iter().all(|filter| filter.contains(entity_id)) { + return Some(entity_id); + } + } + None + } + Self::Difference { left, right } => { + while let Some(entity_id) = left.next_inner() { + if !right.contains(entity_id) { + return Some(entity_id); + } + } + None + } + Self::Union { left, right } => loop { + if let Some(entity_id) = left.next_inner() { + return Some(entity_id); + } + + match right { + UnionRightState::Pending(maybe_set) => { + if let Some(set) = maybe_set.take() { + *right = UnionRightState::Active(Box::new(Self::from_entity_set(set))); + } + continue; + } + UnionRightState::Active(right_iter) => { + while let Some(entity_id) = right_iter.next_inner() { + if !left.contains(entity_id) { + return Some(entity_id); + } + } + return None; + } + } + }, + } + } + + #[inline] + fn size_hint_inner(&self) -> (usize, Option) { + match self { + Self::Empty => (0, Some(0)), + Self::Source(source) => source.size_hint(), + Self::IntersectionSources { driver, .. } => { + let (_, upper) = driver.size_hint(); + (0, upper) + } + Self::Intersection { driver, .. } => { + let (_, upper) = driver.size_hint_inner(); + (0, upper) + } + Self::Difference { left, .. } => { + let (_, upper) = left.size_hint_inner(); + (0, upper) + } + Self::Union { left, right } => { + let (_, left_upper) = left.size_hint_inner(); + let right_upper = match right { + UnionRightState::Pending(_) => None, + UnionRightState::Active(right_iter) => right_iter.size_hint_inner().1, + }; + let upper = match (left_upper, right_upper) { + (Some(a), Some(b)) => Some(a.saturating_add(b)), + _ => None, + }; + (0, upper) + } + } + } +} + /// An iterator over the IDs in an entity set, producing `EntityId`s until exhausted. pub struct EntitySetIterator<'c, E: Entity> { - source: SourceIterator<'c, E>, - sources: Vec>, + inner: EntitySetIteratorInner<'c, E>, } impl<'c, E: Entity> EntitySetIterator<'c, E> { - /// Create a new empty `EntitySetIterator` for situations where you know - /// there are no results but need an `EntitySetIterator`. - pub fn empty() -> EntitySetIterator<'c, E> { + pub(crate) fn empty() -> EntitySetIterator<'c, E> { EntitySetIterator { - source: SourceIterator::Empty, - sources: vec![], + inner: EntitySetIteratorInner::Empty, } } - /// Create a new `EntitySetIterator` that iterates over the entire entity population. - /// This is used, for example, when the query is the empty query. pub(crate) fn from_population_iterator(iter: PopulationIterator) -> Self { EntitySetIterator { - source: SourceIterator::WholePopulation(iter), - sources: vec![], + inner: EntitySetIteratorInner::Source(SourceIterator::Population(iter)), } } - /// Create a new `EntitySetIterator` from a provided list of sources. - /// The sources need not be sorted. pub(crate) fn from_sources(mut sources: Vec>) -> Self { if sources.is_empty() { return Self::empty(); } + if sources.len() == 1 { + return EntitySetIterator { + inner: EntitySetIteratorInner::Source(sources.pop().unwrap().into_iter()), + }; + } - sources.sort_unstable_by_key(|x| x.upper_len()); - let source = sources.remove(0).into_iter(); - EntitySetIterator { source, sources } + // This path constructs intersections from raw source vectors, so we sort here. + // We keep ascending order so filters checked by `all()` are smallest-first. + sources.sort_unstable_by_key(SourceSet::len_upper); + let mut source_iter = sources.into_iter(); + let driver = source_iter.next().unwrap().into_iter(); + EntitySetIterator { + inner: EntitySetIteratorInner::IntersectionSources { + driver, + filters: source_iter.collect(), + }, + } + } + + pub(crate) fn from_index_set(set: Ref<'c, IndexSet>>) -> EntitySetIterator<'c, E> { + EntitySetIterator { + inner: EntitySetIteratorInner::Source(SourceSet::IndexSet(set).into_iter()), + } } - pub fn from_index_set(set: Ref<'c, IndexSet>>) -> EntitySetIterator<'c, E> { + pub(super) fn new(set: EntitySet<'c, E>) -> Self { EntitySetIterator { - source: SourceSet::IndexSet(set).into_iter(), - sources: vec![], + inner: EntitySetIteratorInner::from_entity_set(set), } } @@ -119,60 +316,90 @@ impl<'c, E: Entity> EntitySetIterator<'c, E> { impl<'a, E: Entity> Iterator for EntitySetIterator<'a, E> { type Item = EntityId; + #[inline] fn next(&mut self) -> Option { - // Walk over the iterator and return an entity iff: - // (1) they exist in all the indexes - // (2) they match the unindexed properties - 'outer: for entity_id in self.source.by_ref() { - // (1) check all the indexes - for source in &self.sources { - if !source.contains(entity_id) { - continue 'outer; + self.inner.next_inner() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint_inner() + } + + fn count(self) -> usize { + let EntitySetIterator { inner } = self; + match inner { + EntitySetIteratorInner::Source(source) => source.count(), + EntitySetIteratorInner::IntersectionSources { + mut driver, + filters, + } => driver + .by_ref() + .filter(|&entity_id| filters.iter().all(|filter| filter.contains(entity_id))) + .count(), + other => { + let mut it = EntitySetIterator { inner: other }; + let mut n = 0usize; + while it.next().is_some() { + n += 1; } + n } - - // This entity matches. - return Some(entity_id); } - - None } - fn size_hint(&self) -> (usize, Option) { - let (lower, upper) = self.source.size_hint(); - if self.sources.is_empty() { - (lower, upper) - } else { - // The intersection may be empty but cannot have more than the - // upper bound of the source set. - (0, upper) + #[inline] + fn nth(&mut self, n: usize) -> Option { + match &mut self.inner { + EntitySetIteratorInner::Source(source) => source.nth(n), + EntitySetIteratorInner::IntersectionSources { driver, filters } => driver + .by_ref() + .filter(|&entity_id| filters.iter().all(|filter| filter.contains(entity_id))) + .nth(n), + _ => { + for _ in 0..n { + self.next()?; + } + self.next() + } } } - fn count(self) -> usize { - if self.sources.is_empty() { - // Fast path, as some source types have fast count impls. - self.source.count() - } else { - self.fold(0, |n, _| n + 1) + fn for_each(self, mut f: F) + where + F: FnMut(Self::Item), + { + let EntitySetIterator { inner } = self; + match inner { + EntitySetIteratorInner::Source(source) => source.for_each(f), + other => { + let it = EntitySetIterator { inner: other }; + for item in it { + f(item); + } + } } } - fn nth(&mut self, n: usize) -> Option { - if self.sources.is_empty() { - // Fast path: delegate to the underlying source iterator, - // which may have an optimized `nth` implementation. - self.source.nth(n) - } else { - // General path: advance through the filtered iterator. - // `nth(n)` is equivalent to skipping `n` items and returning the next. - for _ in 0..n { - self.next()?; + fn fold(self, init: B, mut f: F) -> B + where + F: FnMut(B, Self::Item) -> B, + { + let EntitySetIterator { inner } = self; + match inner { + EntitySetIteratorInner::Source(source) => source.fold(init, f), + other => { + let it = EntitySetIterator { inner: other }; + let mut acc = init; + for item in it { + acc = f(acc, item); + } + acc } - self.next() } } } + impl<'c, E: Entity> std::iter::FusedIterator for EntitySetIterator<'c, E> {} #[cfg(test)] @@ -202,8 +429,12 @@ mod tests { set", making the tested property NOT the initial source position. */ + use std::cell::RefCell; + use indexmap::IndexSet; + use crate::entity::entity_set::{EntitySet, SourceSet}; + use crate::hashing::IndexSet as FxIndexSet; use crate::prelude::*; use crate::{define_derived_property, define_property}; @@ -661,4 +892,248 @@ mod tests { .count(); assert_eq!(intersection_count, 1); } + + #[test] + fn test_expression_intersection_iteration() { + let set = EntitySet::from_source(SourceSet::::Population(10)) + .intersection(EntitySet::from_source(SourceSet::::Population(6))); + + let ids = set.into_iter().collect::>(); + let expected = (0..6).map(EntityId::new).collect::>(); + assert_eq!(ids, expected); + } + + #[test] + fn test_expression_difference_iteration() { + let set = EntitySet::from_source(SourceSet::::Population(5)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(2))), + ); + + let ids = set.into_iter().collect::>(); + let expected = vec![ + EntityId::new(0), + EntityId::new(1), + EntityId::new(3), + EntityId::new(4), + ]; + assert_eq!(ids, expected); + } + + #[test] + fn test_expression_union_deduplicates() { + let left = EntitySet::from_source(SourceSet::::Population(3)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(99))), + ); + let right = EntitySet::from_source(SourceSet::::Population(5)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(99))), + ); + let set = left.union(right); + + let ids = set.into_iter().collect::>(); + let expected = (0..5).map(EntityId::new).collect::>(); + assert_eq!(ids, expected); + } + + #[test] + fn test_expression_union_overlap_no_duplicates() { + let left = EntitySet::from_source(SourceSet::::Population(5)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(4))), + ); + let right = EntitySet::from_source(SourceSet::::Population(7)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(0))), + ); + + let ids = left.union(right).into_iter().collect::>(); + let expected = (0..7).map(EntityId::new).collect::>(); + assert_eq!(ids, expected); + } + + #[test] + fn test_expression_intersection_of_unions() { + let ab = EntitySet::from_source(SourceSet::::Entity(EntityId::new(1))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(2), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))); + let cd = EntitySet::from_source(SourceSet::::Entity(EntityId::new(2))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(4), + ))); + + let ids = ab.intersection(cd).into_iter().collect::>(); + assert_eq!(ids, vec![EntityId::new(2), EntityId::new(3)]); + } + + #[test] + fn test_expression_difference_not_commutative() { + let left = EntitySet::from_source(SourceSet::::Entity(EntityId::new(1))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(2), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))); + let right = EntitySet::from_source(SourceSet::::Entity(EntityId::new(2))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(4), + ))); + + let left_minus_right = left.difference(right).into_iter().collect::>(); + let right_minus_left = + EntitySet::from_source(SourceSet::::Entity(EntityId::new(2))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(4), + ))) + .difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(1))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(2), + ))) + .union(EntitySet::from_source(SourceSet::::Entity( + EntityId::new(3), + ))), + ) + .into_iter() + .collect::>(); + + assert_eq!(left_minus_right, vec![EntityId::new(1)]); + assert_eq!(right_minus_left, vec![EntityId::new(4)]); + } + + #[test] + fn test_union_size_hint_pending_right_is_unknown() { + let left = EntitySet::from_source(SourceSet::::Population(2)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(99))), + ); + let right = EntitySet::from_source(SourceSet::::Population(4)).difference( + EntitySet::from_source(SourceSet::::Entity(EntityId::new(98))), + ); + let iter = left.union(right).into_iter(); + + assert_eq!(iter.size_hint(), (0, None)); + } + + #[test] + fn test_nth_and_count_on_source() { + let mut iter = EntitySet::from_source(SourceSet::::Population(5)).into_iter(); + assert_eq!(iter.nth(2), Some(EntityId::new(2))); + + let remaining = iter.count(); + assert_eq!(remaining, 2); + } + + fn finite_set(ids: &[usize]) -> RefCell>> { + RefCell::new( + ids.iter() + .copied() + .map(EntityId::new) + .collect::>(), + ) + } + + fn as_entity_set(set: &RefCell>>) -> EntitySet { + EntitySet::from_source(SourceSet::IndexSet(set.borrow())) + } + + #[test] + fn prototype_empty_entity_set_yields_nothing() { + let ids = EntitySet::::empty().into_iter().collect::>(); + assert!(ids.is_empty()); + } + + #[test] + fn prototype_iter_union_disjoint() { + let a = finite_set(&[1, 2]); + let b = finite_set(&[3, 4]); + let ids = as_entity_set(&a) + .union(as_entity_set(&b)) + .into_iter() + .collect::>(); + let expected = [1usize, 2, 3, 4] + .into_iter() + .map(EntityId::new) + .collect::>(); + assert_eq!(ids, expected); + } + + #[test] + fn prototype_iter_union_overlapping() { + let a = finite_set(&[1, 2, 3]); + let b = finite_set(&[2, 3, 4]); + let ids = as_entity_set(&a) + .union(as_entity_set(&b)) + .into_iter() + .collect::>(); + let unique = ids.iter().copied().collect::>(); + assert_eq!(ids.len(), unique.len()); + assert!(unique.contains(&EntityId::new(1))); + assert!(unique.contains(&EntityId::new(4))); + } + + #[test] + fn prototype_iter_intersection_disjoint() { + let a = finite_set(&[1, 2]); + let b = finite_set(&[3, 4]); + let ids = as_entity_set(&a) + .intersection(as_entity_set(&b)) + .into_iter() + .collect::>(); + assert!(ids.is_empty()); + } + + #[test] + fn prototype_iter_difference_basic() { + let a = finite_set(&[1, 2, 3, 4]); + let b = finite_set(&[3, 4, 5, 6]); + let ids = as_entity_set(&a) + .difference(as_entity_set(&b)) + .into_iter() + .collect::>(); + assert_eq!(ids.len(), 2); + assert!(ids.contains(&EntityId::new(1))); + assert!(ids.contains(&EntityId::new(2))); + } + + #[test] + fn prototype_iter_matches_contains_compound() { + let a = finite_set(&[1, 2, 3, 4]); + let b = finite_set(&[3, 4, 5]); + let c = finite_set(&[7, 8, 9, 10]); + let d = finite_set(&[9, 10, 11]); + let left = as_entity_set(&a).intersection(as_entity_set(&b)); + let right = as_entity_set(&c).difference(as_entity_set(&d)); + let iterated = left.union(right).into_iter().collect::>(); + + let a2 = finite_set(&[1, 2, 3, 4]); + let b2 = finite_set(&[3, 4, 5]); + let c2 = finite_set(&[7, 8, 9, 10]); + let d2 = finite_set(&[9, 10, 11]); + let check = as_entity_set(&a2) + .intersection(as_entity_set(&b2)) + .union(as_entity_set(&c2).difference(as_entity_set(&d2))); + + for value in 0..15 { + let entity = EntityId::new(value); + assert_eq!(iterated.contains(&entity), check.contains(entity)); + } + } + + #[test] + fn prototype_size_hint_single_source_and_partial_consume() { + let mut iter = EntitySet::from_source(SourceSet::::Population(5)).into_iter(); + assert_eq!(iter.size_hint(), (5, Some(5))); + iter.next(); + assert_eq!(iter.size_hint(), (4, Some(4))); + } } diff --git a/src/entity/entity_set/mod.rs b/src/entity/entity_set/mod.rs index 878acd12..d50f2462 100644 --- a/src/entity/entity_set/mod.rs +++ b/src/entity/entity_set/mod.rs @@ -1,5 +1,8 @@ +mod entity_set; mod entity_set_iterator; +mod source_iterator; mod source_set; +pub use entity_set::EntitySet; pub use entity_set_iterator::EntitySetIterator; pub(crate) use source_set::SourceSet; diff --git a/src/entity/entity_set/source_iterator.rs b/src/entity/entity_set/source_iterator.rs new file mode 100644 index 00000000..9c55cb57 --- /dev/null +++ b/src/entity/entity_set/source_iterator.rs @@ -0,0 +1,296 @@ +//! `SourceIterator` is the concrete iteration engine used by +//! `EntitySetIterator`. +//! +//! It encapsulates the concrete source chosen for traversal: an index-set +//! iterator, a property-source iterator, a whole-population iterator, or an +//! empty iterator. +//! +//! `SourceSet` (defined in `source_set.rs`) builds `SourceIterator` values, +//! and `EntitySetIterator` drives them while applying remaining set-membership +//! filters. +//! +//! Depending on source kind, the underlying iterator may be implemented either: +//! - directly in this module (for `IndexSetIterator` and `PopulationIterator`), or +//! - on intermediate wrapper types defined in `source_set.rs` +//! (`ConcretePropertySource` and `DerivedPropertySource`), which serve as both +//! set-facing wrappers and iterators. + +use std::cell::Ref; +use std::fmt::{Debug, Formatter}; + +use ouroboros::self_referencing; + +use super::source_set::AbstractPropertySource; +use crate::entity::{Entity, EntityId, PopulationIterator}; +use crate::hashing::{IndexSet, IndexSetIter}; + +/// The self-referential iterator type for index sets. We don't implement +/// `Iterator` for this struct, choosing instead to access the inner +/// iterator in the `Iterator` implementation on `SourceIterator`. +#[self_referencing] +pub(super) struct IndexSetIterator<'a, E: Entity> { + index_set: Ref<'a, IndexSet>>, + #[borrows(index_set)] + #[covariant] + iter: IndexSetIter<'this, EntityId>, +} + +impl<'a, E: Entity> IndexSetIterator<'a, E> { + pub fn from_index_set(index_set: Ref<'a, IndexSet>>) -> Self { + IndexSetIteratorBuilder { + index_set, + iter_builder: |index_set| index_set.iter(), + } + .build() + } +} + +/// Kinds of iterators that are used as a basis for `EntitySetIterator` +pub(super) enum SourceIterator<'a, E: Entity> { + /// An iterator over an index set + IndexIter(IndexSetIterator<'a, E>), + /// An iterator over a property vector + PropertyVecIter(Box + 'a>), + /// An iterator over the entire population + Population(PopulationIterator), + /// A singleton iterator + Entity { id: EntityId, exhausted: bool }, + /// An empty iterator + Empty, +} + +impl<'a, E: Entity> Debug for SourceIterator<'a, E> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SourceIterator::IndexIter(_iter) => write!(f, "IndexIter"), + SourceIterator::PropertyVecIter(_iter) => write!(f, "PropertyVecIter"), + SourceIterator::Population { .. } => write!(f, "WholePopulation"), + SourceIterator::Entity { .. } => write!(f, "Entity"), + SourceIterator::Empty => write!(f, "Empty"), + } + } +} + +impl<'a, E: Entity> SourceIterator<'a, E> { + /// Test whether `id` is a member of the original source set this iterator was built from. + #[must_use] + #[inline] + pub(super) fn contains(&self, id: EntityId) -> bool { + match self { + SourceIterator::IndexIter(iter) => { + iter.with_index_set(|index_set| index_set.contains(&id)) + } + SourceIterator::PropertyVecIter(source) => source.contains(id), + SourceIterator::Population(source) => id.0 < source.population(), + SourceIterator::Entity { id: entity_id, .. } => *entity_id == id, + SourceIterator::Empty => false, + } + } +} + +impl<'a, E: Entity> Iterator for SourceIterator<'a, E> { + type Item = EntityId; + + #[inline] + fn next(&mut self) -> Option { + match self { + SourceIterator::IndexIter(index_set_iter) => { + index_set_iter.with_iter_mut(|iter| iter.next().copied()) + } + SourceIterator::PropertyVecIter(iter) => iter.next(), + SourceIterator::Population(iter) => iter.next(), + SourceIterator::Entity { id, exhausted } => { + if *exhausted { + None + } else { + *exhausted = true; + Some(*id) + } + } + SourceIterator::Empty => None, + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + SourceIterator::IndexIter(iter) => iter.with_iter(|iter| iter.size_hint()), + SourceIterator::PropertyVecIter(iter) => iter.size_hint(), + SourceIterator::Population(iter) => iter.size_hint(), + SourceIterator::Entity { exhausted, .. } => { + if *exhausted { + (0, Some(0)) + } else { + (1, Some(1)) + } + } + SourceIterator::Empty => (0, Some(0)), + } + } + + fn count(self) -> usize { + // Some of these iterators have very efficient `count` implementations, and we want to exploit + // them when they exist. + match self { + SourceIterator::IndexIter(mut iter) => iter.with_iter_mut(|iter| iter.count()), + SourceIterator::PropertyVecIter(iter) => iter.count(), + SourceIterator::Population(iter) => iter.count(), + SourceIterator::Entity { exhausted, .. } => { + if exhausted { + 0 + } else { + 1 + } + } + SourceIterator::Empty => 0, + } + } + + fn last(self) -> Option { + match self { + Self::IndexIter(mut iter) => iter.with_iter_mut(|iter| iter.last().copied()), + Self::PropertyVecIter(iter) => iter.last(), + Self::Population(iter) => iter.last(), + Self::Entity { id, exhausted } => { + if exhausted { + None + } else { + Some(id) + } + } + Self::Empty => None, + } + } + + #[inline] + fn nth(&mut self, n: usize) -> Option { + match self { + Self::IndexIter(iter) => iter.with_iter_mut(|iter| iter.nth(n).copied()), + Self::PropertyVecIter(iter) => iter.nth(n), + Self::Population(iter) => iter.nth(n), + Self::Entity { id, exhausted } => { + if n == 0 && !*exhausted { + *exhausted = true; + Some(*id) + } else { + *exhausted = true; + None + } + } + Self::Empty => None, + } + } + + fn for_each(self, mut f: F) + where + F: FnMut(Self::Item), + { + match self { + Self::IndexIter(mut iter) => { + iter.with_iter_mut(|iter| iter.for_each(|entity_id| f(*entity_id))) + } + Self::PropertyVecIter(iter) => iter.for_each(f), + Self::Population(iter) => iter.for_each(f), + Self::Entity { id, exhausted } => { + if !exhausted { + f(id); + } + } + Self::Empty => {} + } + } + + fn fold(self, init: B, mut f: F) -> B + where + F: FnMut(B, Self::Item) -> B, + { + match self { + Self::IndexIter(mut iter) => { + iter.with_iter_mut(|iter| iter.fold(init, |acc, entity_id| f(acc, *entity_id))) + } + Self::PropertyVecIter(iter) => iter.fold(init, f), + Self::Population(iter) => iter.fold(init, f), + Self::Entity { id, exhausted } => { + if exhausted { + init + } else { + f(init, id) + } + } + Self::Empty => init, + } + } +} + +impl<'c, E: Entity> std::iter::FusedIterator for SourceIterator<'c, E> {} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + + use super::super::source_set::{ConcretePropertySource, SourceSet}; + use crate::entity::property_value_store_core::RawPropertyValueVec; + use crate::entity::EntityId; + use crate::hashing::IndexSet; + use crate::{define_entity, define_property}; + + define_entity!(Person); + define_property!(struct Age(u8), Person, default_const = Age(0)); + + #[test] + fn source_iterator_contains_for_index_source_uses_original_set() { + let values: RawPropertyValueVec = + [0u8, 3, 2, 3, 4, 5, 3].into_iter().map(Age).collect(); + let people_set: IndexSet> = IndexSet::from_iter([ + EntityId::new(0), + EntityId::new(2), + EntityId::new(3), + EntityId::new(6), + ]); + let people_set = RefCell::new(people_set); + let people_set_ref = people_set.borrow(); + + let mut iter = SourceSet::IndexSet(people_set_ref).into_iter(); + assert_eq!(iter.next(), Some(EntityId::new(0))); + + assert!(iter.contains(EntityId::new(0))); + assert!(iter.contains(EntityId::new(6))); + assert!(!iter.contains(EntityId::new(5))); + + let mut property_iter = SourceSet::PropertySet(Box::new(ConcretePropertySource::< + Person, + Age, + >::new( + &values, Age(3u8), 8 + ))) + .into_iter(); + assert_eq!(property_iter.next(), Some(EntityId::new(1))); + assert!(property_iter.contains(EntityId::new(1))); + assert!(property_iter.contains(EntityId::new(3))); + assert!(!property_iter.contains(EntityId::new(4))); + } + + #[test] + fn source_iterator_contains_for_population_and_empty() { + let mut population_iter = SourceSet::::Population(5).into_iter(); + assert_eq!(population_iter.next(), Some(EntityId::new(0))); + assert_eq!(population_iter.next(), Some(EntityId::new(1))); + + assert!(population_iter.contains(EntityId::new(0))); + assert!(population_iter.contains(EntityId::new(4))); + assert!(!population_iter.contains(EntityId::new(5))); + + let empty_iter = SourceSet::::Empty.into_iter(); + assert!(!empty_iter.contains(EntityId::new(0))); + } + + #[test] + fn source_iterator_contains_for_entity_uses_original_set() { + let mut iter = SourceSet::::Entity(EntityId::new(11)).into_iter(); + assert_eq!(iter.next(), Some(EntityId::new(11))); + + assert!(iter.contains(EntityId::new(11))); + assert!(!iter.contains(EntityId::new(10))); + assert_eq!(iter.next(), None); + } +} diff --git a/src/entity/entity_set/source_set.rs b/src/entity/entity_set/source_set.rs index 4eeaa19c..bb7d7d34 100644 --- a/src/entity/entity_set/source_set.rs +++ b/src/entity/entity_set/source_set.rs @@ -1,39 +1,57 @@ -//! A `SourceSet` abstractly represents the set of `EntityId`s for which a particular -//! `Property` has a particular value. +//! `SourceSet` and `SourceIterator` are companion types that provide a uniform +//! interface over several internal data-source representations used by query +//! execution. //! -//! A `SourceSet` can be converted into a `SourceIterator<'c>`, an -//! iterator over the set of `EntityId`s it represents. The lifetime `'c` -//! is the lifetime of the (immutable) borrow of the underlying `Context`. +//! `SourceSet` is the membership-oriented view (`contains`, `len_upper`), while +//! `SourceIterator` (defined in `source_iterator.rs`) is the traversal-oriented +//! view. A `SourceSet` can be converted into a `SourceIterator<'c>` over the same +//! logical set. The lifetime `'c` is the lifetime of the (immutable) borrow of +//! the underlying `Context`. //! -//! The `SourceSet<'c>` and `SourceIterator<'c>` types are used by `EntitySetIterator<'c>`, which -//! iterates over the intersection of a set of `SourceSet`s. Internally, `EntitySetIterator` holds -//! its state in a set of `SourceSet` instances and a `SourceIterator`, which is an iterator created -//! from a `SourceSet`. A `SourceSet` wraps either an index set (an immutable reference to a set -//! from an index) or a property vector (the `Vec::Value>>` that internally -//! stores the property values) and can compute membership very efficiently. The algorithm chooses -//! the _smallest_ `SourceSet` to create its `SourceIterator` and, when `EntitySetIterator::next()` -//! is called, this `SourceIterator` is iterated over until an ID is found that is contained -//! in all other `SourceSet`s, in which case the ID is returned, or until it is exhausted. +//! Several auxiliary types sit between raw storage and these companion types: +//! - `AbstractPropertySource`: type-erased property-backed set interface +//! - `ConcretePropertySource`: wrapper over concrete property-value vectors +//! - `DerivedPropertySource`: wrapper that computes derived property values +//! - `IndexSetIterator` (in `source_iterator.rs`): self-referential wrapper over index-set iterators +//! +//! In some cases we intentionally do not split into separate intermediate +//! `*Set` and `*Iterator` wrappers. For simplicity, `ConcretePropertySource` and +//! `DerivedPropertySource` each implement both the set-facing +//! `AbstractPropertySource` API and `Iterator`. +//! +//! `EntitySetIterator<'c>` uses `SourceSet<'c>` and `SourceIterator<'c>` to +//! evaluate intersections. Sources may be empty, whole-population, index-backed, +//! singleton, or property-backed. The iterator chooses the smallest source as the driver and +//! checks candidate IDs against remaining sources. use std::cell::Ref; -use std::fmt::{Debug, Formatter}; use std::marker::PhantomData; -use ouroboros::self_referencing; - +use super::source_iterator::{IndexSetIterator, SourceIterator}; use crate::entity::index::IndexSetResult; use crate::entity::property_value_store_core::RawPropertyValueVec; use crate::entity::{ContextEntitiesExt, Entity, EntityId, PopulationIterator}; -use crate::hashing::{IndexSet, IndexSetIter}; +use crate::hashing::{HashValueType, IndexSet}; use crate::prelude::Property; use crate::Context; -type BxPropertySource<'a, E> = Box + 'a>; +pub(super) type BxPropertySource<'a, E> = Box + 'a>; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct PropertySourceId { + pub property_id: usize, + pub value_hash: HashValueType, +} /// Type erased property source representing the (abstract) set of `EntityId`s /// for which a particular property has a particular value. This is used for /// both `ConcretePropertyVec<'a, P: Property>` and `DerivedPropertySource<'a, P: Property>`. -pub trait AbstractPropertySource<'a, E: Entity> { +pub(crate) trait AbstractPropertySource<'a, E: Entity>: + Iterator> +{ + /// Identity of the logical property query represented by this source. + fn id(&self) -> PropertySourceId; + /// An upper bound on the number of elements this source will need to iterate over. The idea is to perform the /// minimum amount of work to determine the first set in the intersection of sets that `EntitySetIterator` /// represents. @@ -42,10 +60,6 @@ pub trait AbstractPropertySource<'a, E: Entity> { /// A test that `entity_id` is contained in the (abstractly /// defined) set. This operation is very efficient. fn contains(&self, entity_id: EntityId) -> bool; - - /// This is purely a type cast from `Box` to - /// `Box>>`. Notice the type of `self`. - fn to_iter(self: Box) -> Box> + 'a>; } /// To iterate over the values of an unindexed derived property, @@ -83,6 +97,13 @@ impl<'a, E: Entity, P: Property> DerivedPropertySource<'a, E, P> { impl<'a, E: Entity, P: Property> AbstractPropertySource<'a, E> for DerivedPropertySource<'a, E, P> { + fn id(&self) -> PropertySourceId { + PropertySourceId { + property_id: P::index_id(), + value_hash: P::hash_property_value(&self.value.make_canonical()), + } + } + fn upper_len(&self) -> usize { self.context.get_entity_count::() } @@ -90,10 +111,6 @@ impl<'a, E: Entity, P: Property> AbstractPropertySource<'a, E> fn contains(&self, entity_id: EntityId) -> bool { P::compute_derived(self.context, entity_id) == self.value } - - fn to_iter(self: Box) -> Box> + 'a> { - self - } } impl<'a, E: Entity, P: Property> Iterator for DerivedPropertySource<'a, E, P> { @@ -158,6 +175,13 @@ impl<'a, E: Entity, P: Property> ConcretePropertySource<'a, E, P> { impl<'a, E: Entity, P: Property> AbstractPropertySource<'a, E> for ConcretePropertySource<'a, E, P> { + fn id(&self) -> PropertySourceId { + PropertySourceId { + property_id: P::index_id(), + value_hash: P::hash_property_value(&self.value.make_canonical()), + } + } + fn upper_len(&self) -> usize { if !self.is_default_value { self.values.len() @@ -177,10 +201,6 @@ impl<'a, E: Entity, P: Property> AbstractPropertySource<'a, E> self.is_default_value } } - - fn to_iter(self: Box) -> Box> + 'a> { - self - } } impl<'a, E: Entity, P: Property> Iterator for ConcretePropertySource<'a, E, P> { @@ -213,42 +233,38 @@ impl<'a, E: Entity, P: Property> Iterator for ConcretePropertySource<'a, E, P } } -/// The self-referential iterator type for index sets. We don't implement -/// `Iterator` for this struct, choosing instead to access the inner -/// iterator in the `Iterator` implementation on `SourceIterator`. -#[self_referencing] -pub(super) struct IndexSetIterator<'a, E: Entity> { - index_set: Ref<'a, IndexSet>>, - #[borrows(index_set)] - #[covariant] - iter: IndexSetIter<'this, EntityId>, +/// Represents the set of `EntityId`s for which a particular `Property` has a particular value. +pub(crate) enum SourceSet<'a, E: Entity> { + Empty, + Population(usize), + #[allow(dead_code)] + Entity(EntityId), + IndexSet(Ref<'a, IndexSet>>), + PropertySet(BxPropertySource<'a, E>), } -impl<'a, E: Entity> IndexSetIterator<'a, E> { - pub fn from_index_set(index_set: Ref<'a, IndexSet>>) -> Self { - IndexSetIteratorBuilder { - index_set, - iter_builder: |index_set| index_set.iter(), +impl<'a, E: Entity> PartialEq for SourceSet<'a, E> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Empty, Self::Empty) => true, + (Self::Population(left), Self::Population(right)) => left == right, + (Self::Entity(left), Self::Entity(right)) => left == right, + (Self::IndexSet(left), Self::IndexSet(right)) => std::ptr::eq(&**left, &**right), + (Self::PropertySet(left), Self::PropertySet(right)) => left.id() == right.id(), + _ => false, } - .build() } } -/// Represents the set of `EntityId`s for which a particular `Property` has a particular value. -pub(crate) enum SourceSet<'a, E: Entity> { - IndexSet(Ref<'a, IndexSet>>), - PropertySet(BxPropertySource<'a, E>), -} +impl<'a, E: Entity> Eq for SourceSet<'a, E> {} impl<'a, E: Entity> SourceSet<'a, E> { - /// A constructor for `SourceSet`s during construction of `EntitySetIterator` in - /// `Query::new_query_result_iterator()`. Returns `None` if the set is empty. + /// A constructor for `SourceSet`s during construction of `EntitySet` in + /// `Query::new_query_result()`. Returns `None` if the set is empty. /// /// We first look for an index set. If not found, we check if the property is derived. /// For derived properties, we wrap a reference to the `Context`. For nonderived /// properties, we wrap a reference to the property's backing vector. - /// - /// This method refreshes outdated indexes. pub(crate) fn new>(value: P, context: &'a Context) -> Option { let property_store = context.entity_store.get_property_store::(); @@ -293,8 +309,11 @@ impl<'a, E: Entity> SourceSet<'a, E> { } } - pub(super) fn upper_len(&self) -> usize { + pub(super) fn len_upper(&self) -> usize { match self { + SourceSet::Empty => 0, + SourceSet::Population(population) => *population, + SourceSet::Entity(_) => 1, SourceSet::IndexSet(source) => source.len(), SourceSet::PropertySet(source) => source.upper_len(), } @@ -302,6 +321,9 @@ impl<'a, E: Entity> SourceSet<'a, E> { pub(super) fn contains(&self, id: EntityId) -> bool { match self { + SourceSet::Empty => false, + SourceSet::Population(population) => id.0 < *population, + SourceSet::Entity(entity_id) => *entity_id == id, SourceSet::IndexSet(source) => source.contains(&id), SourceSet::PropertySet(source) => source.contains(id), } @@ -309,78 +331,18 @@ impl<'a, E: Entity> SourceSet<'a, E> { pub(super) fn into_iter(self) -> SourceIterator<'a, E> { match self { + SourceSet::Empty => SourceIterator::Empty, + SourceSet::Population(population) => { + SourceIterator::Population(PopulationIterator::new(population)) + } + SourceSet::Entity(entity_id) => SourceIterator::Entity { + id: entity_id, + exhausted: false, + }, SourceSet::IndexSet(ids) => { SourceIterator::IndexIter(IndexSetIterator::from_index_set(ids)) } - SourceSet::PropertySet(property_vec) => { - SourceIterator::PropertyVecIter(property_vec.to_iter()) - } - } - } -} -/// Kinds of iterators that are used as a basis for `EntitySetIterator` -pub(super) enum SourceIterator<'a, E: Entity> { - /// An iterator over an index set - IndexIter(IndexSetIterator<'a, E>), - /// An iterator over a property vector - PropertyVecIter(Box> + 'a>), - /// An iterator over the entire population - WholePopulation(PopulationIterator), - /// An empty iterator - Empty, -} - -impl<'a, E: Entity> Debug for SourceIterator<'a, E> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SourceIterator::IndexIter(_iter) => write!(f, "IndexIter"), - SourceIterator::PropertyVecIter(_iter) => write!(f, "PropertyVecIter"), - SourceIterator::WholePopulation(_iter) => write!(f, "WholePopulation"), - SourceIterator::Empty => write!(f, "Empty"), - } - } -} - -impl<'a, E: Entity> Iterator for SourceIterator<'a, E> { - type Item = EntityId; - - fn next(&mut self) -> Option { - match self { - SourceIterator::IndexIter(index_set_iter) => { - index_set_iter.with_iter_mut(|iter| iter.next().copied()) - } - SourceIterator::PropertyVecIter(iter) => iter.next(), - SourceIterator::WholePopulation(iter) => iter.next(), - SourceIterator::Empty => None, - } - } - - fn size_hint(&self) -> (usize, Option) { - match self { - SourceIterator::IndexIter(iter) => iter.with_iter(|iter| iter.size_hint()), - SourceIterator::PropertyVecIter(_) => (0, None), - SourceIterator::WholePopulation(iter) => iter.size_hint(), - SourceIterator::Empty => (0, Some(0)), - } - } - - fn count(self) -> usize { - // Some of these iterators have very efficient `count` implementations, and we want to exploit - // them when they exist. - match self { - SourceIterator::IndexIter(mut iter) => iter.with_iter_mut(|iter| iter.count()), - SourceIterator::PropertyVecIter(iter) => iter.count(), - SourceIterator::WholePopulation(iter) => iter.count(), - SourceIterator::Empty => 0, - } - } - - fn nth(&mut self, n: usize) -> Option { - match self { - SourceIterator::IndexIter(iter) => iter.with_iter_mut(|iter| iter.nth(n).copied()), - SourceIterator::PropertyVecIter(iter) => iter.nth(n), - SourceIterator::WholePopulation(iter) => iter.nth(n), - SourceIterator::Empty => None, + SourceSet::PropertySet(property_vec) => SourceIterator::PropertyVecIter(property_vec), } } } @@ -390,38 +352,99 @@ mod tests { use std::cell::RefCell; use super::*; - use crate::{define_entity, define_property}; + use crate::{define_derived_property, define_entity, define_property}; define_entity!(Person); define_property!(struct Age(u8), Person, default_const = Age(0)); + define_property!(struct Flag(bool), Person, default_const = Flag(false)); + define_derived_property!(struct IsAdult(bool), Person, [Age], |age| IsAdult(age.0 >= 18)); #[test] - fn test_iterators() { - let values: RawPropertyValueVec = - [0u8, 3, 2, 3, 4, 5, 3].into_iter().map(Age).collect(); - let people_set = IndexSet::from_iter([ - EntityId::new(0), - EntityId::new(2), - EntityId::new(3), - EntityId::new(6), - ]); - let people_set = RefCell::new(people_set); - let people_set_ref = people_set.borrow(); - { - let pvi = SourceSet::PropertySet(Box::new(ConcretePropertySource::<_, Age>::new( - &values, - Age(3u8), - 8, - ))); - let isi = SourceSet::IndexSet(people_set_ref); - let sources = vec![pvi, isi]; - - for source in sources { - for id in source.into_iter() { - print!("{:?}, ", id); - } - println!(); - } + fn source_set_variants_basic_behavior() { + let empty = SourceSet::::Empty; + assert_eq!(empty.len_upper(), 0); + assert!(!empty.contains(EntityId::new(0))); + assert_eq!(empty.into_iter().count(), 0); + + let population = SourceSet::::Population(3); + assert_eq!(population.len_upper(), 3); + assert!(population.contains(EntityId::new(0))); + assert!(!population.contains(EntityId::new(3))); + let population_ids = SourceSet::::Population(3) + .into_iter() + .collect::>(); + assert_eq!( + population_ids, + vec![EntityId::new(0), EntityId::new(1), EntityId::new(2)] + ); + + let singleton = SourceSet::::Entity(EntityId::new(9)); + assert_eq!(singleton.len_upper(), 1); + assert!(singleton.contains(EntityId::new(9))); + assert!(!singleton.contains(EntityId::new(8))); + let singleton_ids = SourceSet::::Entity(EntityId::new(9)) + .into_iter() + .collect::>(); + assert_eq!(singleton_ids, vec![EntityId::new(9)]); + + let ids = RefCell::new( + [EntityId::new(1), EntityId::new(4), EntityId::new(8)] + .into_iter() + .collect::>(), + ); + let ids_ref = ids.borrow(); + let indexed = SourceSet::::IndexSet(ids_ref); + assert_eq!(indexed.len_upper(), 3); + assert!(indexed.contains(EntityId::new(4))); + assert!(!indexed.contains(EntityId::new(2))); + } + + #[test] + fn source_set_new_uses_indexed_or_unindexed_backing() { + let mut context = Context::new(); + for age in [1u8, 2, 2, 3] { + context.add_entity((Age(age), Flag(true))).unwrap(); } + + assert!(matches!( + SourceSet::::new::(Age(2), &context).unwrap(), + SourceSet::PropertySet(_) + )); + let unindexed_ids = SourceSet::::new::(Age(2), &context) + .unwrap() + .into_iter() + .collect::>(); + + context.index_property::(); + assert!(matches!( + SourceSet::::new::(Age(2), &context).unwrap(), + SourceSet::IndexSet(_) + )); + + let indexed_ids = SourceSet::::new::(Age(2), &context) + .unwrap() + .into_iter() + .collect::>(); + assert_eq!(unindexed_ids, indexed_ids); + } + + #[test] + fn source_set_new_derived_vs_nonderived_backing() { + let mut context = Context::new(); + context.add_entity((Age(12), Flag(true))).unwrap(); + context.add_entity((Age(20), Flag(true))).unwrap(); + context.add_entity((Age(44), Flag(false))).unwrap(); + + let nonderived = SourceSet::::new::(Age(20), &context).unwrap(); + assert!(matches!(nonderived, SourceSet::PropertySet(_))); + + let derived = SourceSet::::new::(IsAdult(true), &context).unwrap(); + assert!(matches!(derived, SourceSet::PropertySet(_))); + + let derived_ids = SourceSet::::new::(IsAdult(true), &context) + .unwrap() + .into_iter() + .collect::>(); + assert_eq!(derived_ids, vec![EntityId::new(1), EntityId::new(2)]); } } diff --git a/src/entity/index/full_index.rs b/src/entity/index/full_index.rs index b370acab..89f2dd96 100644 --- a/src/entity/index/full_index.rs +++ b/src/entity/index/full_index.rs @@ -141,13 +141,13 @@ mod tests { let mut results_a = Default::default(); context.with_query_results((Age(1u8), Weight(2u8), Height(3u8)), &mut |results| { - results_a = results.clone() + results_a = results.into_iter().collect::>() }); assert_eq!(results_a.len(), 1); let mut results_b = Default::default(); context.with_query_results((Weight(2u8), Height(3u8), Age(1u8)), &mut |results| { - results_b = results.clone() + results_b = results.into_iter().collect::>() }); assert_eq!(results_b.len(), 1); @@ -160,13 +160,13 @@ mod tests { let mut results_a = Default::default(); context.with_query_results((Weight(1u8), Height(2u8), Age(3u8)), &mut |results| { - results_a = results.clone() + results_a = results.into_iter().collect::>() }); assert_eq!(results_a.len(), 1); let mut results_b = Default::default(); context.with_query_results((Age(3u8), Weight(1u8), Height(2u8)), &mut |results| { - results_b = results.clone() + results_b = results.into_iter().collect::>() }); assert_eq!(results_b.len(), 1); diff --git a/src/entity/property_impl_tests.rs b/src/entity/property_impl_tests.rs deleted file mode 100644 index 5a00e72a..00000000 --- a/src/entity/property_impl_tests.rs +++ /dev/null @@ -1,263 +0,0 @@ -// The macro for this is found in ixa-macros/property_impl.rs - -// We define unused properties to test macro implementation. -#![allow(dead_code)] - -use ixa::entity::Query; -use ixa::prelude::*; -use serde::Serialize; - -define_entity!(Person); -define_entity!(Group); - -define_property!(struct Pu32(u32), Person, default_const = Pu32(0)); -define_property!(struct POu32(Option), Person, default_const = POu32(None)); -define_property!(struct Name(&'static str), Person, default_const = Name("")); -define_property!(struct Age(u8), Person, default_const = Age(0)); -define_property!(struct Weight(f64), Person, default_const = Weight(0.0)); - -// A struct with named fields -define_property!( - struct Innocculation { - time: f64, - dose: u8, - }, - Person, - default_const = Innocculation { time: 0.0, dose: 0 } -); - -// An enum non-derived property -define_property!( - enum InfectionStatus { - Susceptible, - Infected, - Recovered, - }, - Person, - default_const = InfectionStatus::Susceptible -); - -// An enum derived property -define_derived_property!( - enum AgeGroup { - Child, - Adult, - Senior, - }, - Person, - [Age], // Depends only on age - |age| { - let age: Age = age; - if age.0 < 18 { - AgeGroup::Child - } else if age.0 < 65 { - AgeGroup::Adult - } else { - AgeGroup::Senior - } - } -); - -// Derived property - computed from other properties -define_derived_property!(struct DerivedProp(bool), Person, [Age], - |age| { - DerivedProp(age.0 % 2 == 0) - } -); - -// A property type for two distinct entities. -#[derive(Debug, PartialEq, Clone, Copy, Serialize)] -pub enum InfectionKind { - Respiratory, - Genetic, - Superficial, -} -impl_property!( - InfectionKind, - Person, - default_const = InfectionKind::Respiratory -); -impl_property!(InfectionKind, Group, default_const = InfectionKind::Genetic); - -define_multi_property!((Name, Age, Weight), Person); -define_multi_property!((Age, Weight, Name), Person); -define_multi_property!((Weight, Age, Name), Person); - -// For convenience -type ProfileNAW = (Name, Age, Weight); -type ProfileAWN = (Age, Weight, Name); -type ProfileWAN = (Weight, Age, Name); - -#[test] -fn test_multi_property_ordering() { - let a = (Name("Jane"), Age(22), Weight(180.5)); - let b = (Age(22), Weight(180.5), Name("Jane")); - let c = (Weight(180.5), Age(22), Name("Jane")); - - // Multi-properties share the same index - assert_eq!(ProfileNAW::index_id(), ProfileAWN::index_id()); - assert_eq!(ProfileNAW::index_id(), ProfileWAN::index_id()); - - let a_canonical: >::CanonicalValue = ProfileNAW::make_canonical(a); - let b_canonical: >::CanonicalValue = ProfileAWN::make_canonical(b); - let c_canonical: >::CanonicalValue = ProfileWAN::make_canonical(c); - - assert_eq!(a_canonical, b_canonical); - assert_eq!(a_canonical, c_canonical); - - // Actually, all of the `Profile***::hash_property_value` methods should be the same, - // so we could use any single one. - assert_eq!( - ProfileNAW::hash_property_value(&a_canonical), - ProfileAWN::hash_property_value(&b_canonical) - ); - assert_eq!( - ProfileNAW::hash_property_value(&a_canonical), - ProfileWAN::hash_property_value(&c_canonical) - ); - - // Since the canonical values are the same, we could have used any single one, but this - // demonstrates that we can convert from one order to another. - assert_eq!(ProfileNAW::make_uncanonical(b_canonical), a); - assert_eq!(ProfileAWN::make_uncanonical(c_canonical), b); - assert_eq!(ProfileWAN::make_uncanonical(a_canonical), c); -} - -#[test] -fn test_multi_property_vs_property_query() { - let mut context = Context::new(); - - context - .add_entity((Name("John"), Age(42), Weight(220.5))) - .unwrap(); - context - .add_entity((Name("Jane"), Age(22), Weight(180.5))) - .unwrap(); - context - .add_entity((Name("Bob"), Age(32), Weight(190.5))) - .unwrap(); - context - .add_entity((Name("Alice"), Age(22), Weight(170.5))) - .unwrap(); - - context.index_property::<_, ProfileNAW>(); - - // Check that all equivalent multi-properties are indexed... - assert!(context.is_property_indexed::()); - assert!(context.is_property_indexed::()); - assert!(context.is_property_indexed::()); - // ...but only one `Index` instance was created. - let mut indexed_count = 0; - if context - .get_property_value_store::() - .index_type() - != ixa::entity::index::PropertyIndexType::Unindexed - { - indexed_count += 1; - } - if context - .get_property_value_store::() - .index_type() - != ixa::entity::index::PropertyIndexType::Unindexed - { - indexed_count += 1; - } - if context - .get_property_value_store::() - .index_type() - != ixa::entity::index::PropertyIndexType::Unindexed - { - indexed_count += 1; - } - assert_eq!(indexed_count, 1); - - { - let example_query = (Name("Alice"), Age(22), Weight(170.5)); - let query_multi_property_id = - <(Name, Age, Weight) as Query>::multi_property_id(&example_query); - assert!(query_multi_property_id.is_some()); - assert_eq!(ProfileNAW::index_id(), query_multi_property_id.unwrap()); - assert_eq!( - Query::multi_property_value_hash(&example_query), - ProfileNAW::hash_property_value( - &(Name("Alice"), Age(22), Weight(170.5)).make_canonical() - ) - ); - } - - context.with_query_results(((Name("John"), Age(42), Weight(220.5)),), &mut |results| { - assert_eq!(results.len(), 1); - }); -} - -#[test] -fn test_derived_property() { - let mut context = Context::new(); - - let senior = context.add_entity::((Age(92),)).unwrap(); - let child = context.add_entity::((Age(12),)).unwrap(); - let adult = context.add_entity::((Age(44),)).unwrap(); - - let senior_group: AgeGroup = context.get_property(senior); - let child_group: AgeGroup = context.get_property(child); - let adult_group: AgeGroup = context.get_property(adult); - - assert_eq!(senior_group, AgeGroup::Senior); - assert_eq!(child_group, AgeGroup::Child); - assert_eq!(adult_group, AgeGroup::Adult); - - // Age has no dependencies (only dependents) - assert!(Age::non_derived_dependencies().is_empty()); - // AgeGroup depends only on Age - assert_eq!(AgeGroup::non_derived_dependencies(), [Age::id()]); - - // Age has several dependents. This assert may break if you add or remove the properties in this test module. - let mut expected_dependents = [ - AgeGroup::id(), - DerivedProp::id(), - ProfileNAW::id(), - ProfileAWN::id(), - ProfileWAN::id(), - ]; - expected_dependents.sort_unstable(); - assert_eq!(Age::dependents(), expected_dependents); -} - -#[test] -fn test_get_display() { - let mut context = Context::new(); - let person = context.add_entity((POu32(Some(42)), Pu32(22))).unwrap(); - assert_eq!( - format!( - "{:}", - POu32::get_display(&context.get_property::<_, POu32>(person)) - ), - "42" - ); - assert_eq!( - format!( - "{:}", - Pu32::get_display(&context.get_property::<_, Pu32>(person)) - ), - "Pu32(22)" - ); - let person2 = context.add_entity((POu32(None), Pu32(11))).unwrap(); - assert_eq!( - format!( - "{:}", - POu32::get_display(&context.get_property::<_, POu32>(person2)) - ), - "None" - ); -} - -#[test] -fn test_debug_trait() { - let property = Pu32(11); - let debug_str = format!("{:?}", property); - assert_eq!(debug_str, "Pu32(11)"); - - let property = POu32(Some(22)); - let debug_str = format!("{:?}", property); - assert_eq!(debug_str, "POu32(Some(22))"); -} diff --git a/src/entity/query/mod.rs b/src/entity/query/mod.rs index 260aac3b..2b33fbf9 100644 --- a/src/entity/query/mod.rs +++ b/src/entity/query/mod.rs @@ -4,7 +4,7 @@ use std::any::TypeId; use std::marker::PhantomData; use std::sync::{Mutex, OnceLock}; -use crate::entity::entity_set::EntitySetIterator; +use crate::entity::entity_set::{EntitySet, EntitySetIterator}; use crate::entity::multi_property::type_ids_to_multi_property_index; use crate::entity::property_list::PropertyList; use crate::entity::property_store::PropertyStore; @@ -92,8 +92,8 @@ impl> Query for EntityPropertyTuple { self.inner.multi_property_value_hash() } - fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { - self.inner.new_query_result_iterator(context) + fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> { + self.inner.new_query_result(context) } fn match_entity(&self, entity_id: EntityId, context: &Context) -> bool { @@ -155,8 +155,13 @@ pub trait Query: Copy + 'static { /// multi-property value. fn multi_property_value_hash(&self) -> HashValueType; + /// Creates a new query result as an `EntitySet`. + fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E>; + /// Creates a new `EntitySetIterator`. - fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E>; + fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { + self.new_query_result(context).into_iter() + } /// Determines if the given person matches this query. fn match_entity(&self, entity_id: EntityId, context: &Context) -> bool; @@ -168,7 +173,6 @@ pub trait Query: Copy + 'static { #[cfg(test)] mod tests { - use crate::hashing::HashSetExt; use crate::prelude::*; use crate::{ define_derived_property, define_entity, define_multi_property, define_property, Context, @@ -195,7 +199,7 @@ mod tests { let _ = context.add_entity((RiskCategory::High,)).unwrap(); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -204,7 +208,7 @@ mod tests { let context = Context::new(); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 0); + assert_eq!(people.into_iter().count(), 0); }); } @@ -231,7 +235,7 @@ mod tests { assert!(context.is_property_indexed::()); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -241,7 +245,7 @@ mod tests { let _ = context.add_entity((RiskCategory::High,)); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); assert!(!context.is_property_indexed::()); @@ -249,7 +253,7 @@ mod tests { assert!(context.is_property_indexed::()); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -259,20 +263,20 @@ mod tests { let person1 = context.add_entity((RiskCategory::High,)).unwrap(); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); context.with_query_results((RiskCategory::Low,), &mut |people| { - assert_eq!(people.len(), 0); + assert_eq!(people.into_iter().count(), 0); }); context.set_property(person1, RiskCategory::Low); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 0); + assert_eq!(people.into_iter().count(), 0); }); context.with_query_results((RiskCategory::Low,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -283,7 +287,7 @@ mod tests { context.index_property::(); assert!(context.is_property_indexed::()); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -294,12 +298,12 @@ mod tests { context.index_property::(); assert!(context.is_property_indexed::()); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); let _ = context.add_entity((RiskCategory::High,)).unwrap(); context.with_query_results((RiskCategory::High,), &mut |people| { - assert_eq!(people.len(), 2); + assert_eq!(people.into_iter().count(), 2); }); } @@ -309,7 +313,7 @@ mod tests { let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); context.with_query_results((Age(42),), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -321,7 +325,7 @@ mod tests { let _ = context.add_entity((Age(40), RiskCategory::Low)).unwrap(); context.with_query_results((Age(42), RiskCategory::High), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -333,7 +337,7 @@ mod tests { let _ = context.add_entity((Age(40), RiskCategory::Low)).unwrap(); context.with_query_results((Age(42), RiskCategory::High), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -346,7 +350,7 @@ mod tests { context.index_property::(); context.with_query_results((Age(42), RiskCategory::High), &mut |people| { - assert_eq!(people.len(), 1); + assert_eq!(people.into_iter().count(), 1); }); } @@ -449,50 +453,50 @@ mod tests { // 'regular' derived property context.with_query_results((Ach(28, 2, 160),), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p4)); - assert!(people.contains(&p5)); + assert!(people.contains(p4)); + assert!(people.contains(p5)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); // multi-property index context.with_query_results((Age(28), County(2), Height(160)), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p4)); - assert!(people.contains(&p5)); + assert!(people.contains(p4)); + assert!(people.contains(p5)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); // multi-property index with different order context.with_query_results((County(2), Height(160), Age(28)), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p4)); - assert!(people.contains(&p5)); + assert!(people.contains(p4)); + assert!(people.contains(p5)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); // multi-property index with different order context.with_query_results((Height(160), County(2), Age(28)), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p4)); - assert!(people.contains(&p5)); + assert!(people.contains(p4)); + assert!(people.contains(p5)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); // multi-property index with different order and different value context.with_query_results((Height(140), County(1), Age(28)), &mut |people| { - assert_eq!(people.len(), 1, "Should have 1 matches"); - assert!(people.contains(&p3)); + assert!(people.contains(p3)); + assert_eq!(people.into_iter().count(), 1, "Should have 1 matches"); }); context.set_property(p2, Age(28)); // multi-property index again after changing the value context.with_query_results((Height(140), County(1), Age(28)), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p2)); - assert!(people.contains(&p3)); + assert!(people.contains(p2)); + assert!(people.contains(p3)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); context.with_query_results((Height(140), County(1)), &mut |people| { - assert_eq!(people.len(), 2, "Should have 2 matches"); - assert!(people.contains(&p2)); - assert!(people.contains(&p3)); + assert!(people.contains(p2)); + assert!(people.contains(p3)); + assert_eq!(people.into_iter().count(), 2, "Should have 2 matches"); }); } @@ -567,8 +571,8 @@ mod tests { EntityPropertyTuple::new((Age(42), RiskCategory::High)); context.with_query_results(query, &mut |people| { - assert_eq!(people.len(), 1); - assert!(people.contains(&p1)); + assert!(people.contains(p1)); + assert_eq!(people.into_iter().count(), 1); }); // Test match_entity @@ -666,7 +670,7 @@ mod tests { assert_eq!(context.query_entity_count(query), 1); context.with_query_results(query, &mut |people| { - assert!(people.contains(&p1)); + assert!(people.contains(p1)); }); } diff --git a/src/entity/query/query_impls.rs b/src/entity/query/query_impls.rs index 5aa29401..c761354b 100644 --- a/src/entity/query/query_impls.rs +++ b/src/entity/query/query_impls.rs @@ -2,7 +2,7 @@ use std::any::TypeId; use seq_macro::seq; -use crate::entity::entity_set::{EntitySetIterator, SourceSet}; +use crate::entity::entity_set::{EntitySet, EntitySetIterator, SourceSet}; use crate::entity::index::IndexSetResult; use crate::entity::multi_property::static_reorder_by_keys; use crate::entity::property::Property; @@ -28,9 +28,12 @@ impl Query for () { one_shot_128(&empty) } + fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> { + EntitySet::from_source(SourceSet::Population(context.get_entity_count::())) + } + fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { - let population_iterator = context.get_entity_iterator::(); - EntitySetIterator::from_population_iterator(population_iterator) + EntitySetIterator::from_population_iterator(context.get_entity_iterator::()) } fn match_entity(&self, _entity_id: EntityId, _context: &Context) -> bool { @@ -64,7 +67,7 @@ impl> Query for (P1,) { P1::hash_property_value(&P1::make_canonical(self.0)) } - fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { + fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> { let property_store = context.entity_store.get_property_store::(); // The case of an indexed multi-property. @@ -78,10 +81,10 @@ impl> Query for (P1,) { self.multi_property_value_hash(), ) { IndexSetResult::Set(people_set) => { - return EntitySetIterator::from_index_set(people_set); + return EntitySet::from_source(SourceSet::IndexSet(people_set)); } IndexSetResult::Empty => { - return EntitySetIterator::empty(); + return EntitySet::empty(); } IndexSetResult::Unsupported => {} } @@ -95,6 +98,38 @@ impl> Query for (P1,) { sources.push(source_set); } else { // If a single source set is empty, the intersection of all sources is empty. + return EntitySet::empty(); + } + + EntitySet::from_intersection_sources(sources) + } + + fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { + // Constructing the `EntitySetIterator` directly instead of constructing an `EntitySet` + // first is a micro-optimization improving tight-loop benchmark performance by ~10%. + let property_store = context.entity_store.get_property_store::(); + + if let Some(multi_property_id) = self.multi_property_id() { + match property_store.get_index_set_with_hash_for_property_id( + context, + multi_property_id, + self.multi_property_value_hash(), + ) { + IndexSetResult::Set(people_set) => { + return EntitySetIterator::from_index_set(people_set); + } + IndexSetResult::Empty => { + return EntitySetIterator::empty(); + } + IndexSetResult::Unsupported => {} + } + } + + let mut sources: Vec> = Vec::new(); + + if let Some(source_set) = SourceSet::new::(self.0, context) { + sources.push(source_set); + } else { return EntitySetIterator::empty(); } @@ -177,7 +212,7 @@ macro_rules! impl_query { one_shot_128(&data.as_slice()) } - fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { + fn new_query_result<'c>(&self, context: &'c Context) -> EntitySet<'c, E> { // The case of an indexed multi-property. // This mirrors the indexed case in `SourceSet<'a, E>::new()`. The difference is, if the // multi-property is unindexed, we fall through to create `SourceSet`s for the components @@ -190,10 +225,10 @@ macro_rules! impl_query { self.multi_property_value_hash(), ) { $crate::entity::index::IndexSetResult::Set(entity_set) => { - return EntitySetIterator::from_index_set(entity_set); + return EntitySet::from_source(SourceSet::IndexSet(entity_set)); } $crate::entity::index::IndexSetResult::Empty => { - return EntitySetIterator::empty(); + return EntitySet::empty(); } $crate::entity::index::IndexSetResult::Unsupported => {} } @@ -208,6 +243,39 @@ macro_rules! impl_query { sources.push(source_set); } else { // If a single source set is empty, the intersection of all sources is empty. + return EntitySet::empty(); + } + )* + + EntitySet::from_intersection_sources(sources) + } + + fn new_query_result_iterator<'c>(&self, context: &'c Context) -> EntitySetIterator<'c, E> { + // Constructing the `EntitySetIterator` directly instead of constructing an `EntitySet` + // first is a micro-optimization improving tight-loop benchmark performance by ~10%. + if let Some(multi_property_id) = self.multi_property_id() { + let property_store = context.entity_store.get_property_store::(); + match property_store.get_index_set_with_hash_for_property_id( + context, + multi_property_id, + self.multi_property_value_hash(), + ) { + $crate::entity::index::IndexSetResult::Set(entity_set) => { + return EntitySetIterator::from_index_set(entity_set); + } + $crate::entity::index::IndexSetResult::Empty => { + return EntitySetIterator::empty(); + } + $crate::entity::index::IndexSetResult::Unsupported => {} + } + } + + let mut sources: Vec> = Vec::new(); + + #( + if let Some(source_set) = SourceSet::new::(self.N, context) { + sources.push(source_set); + } else { return EntitySetIterator::empty(); } )* diff --git a/src/macros/property_impl.rs b/src/macros/property_impl.rs index 4ab57deb..f5c118e3 100644 --- a/src/macros/property_impl.rs +++ b/src/macros/property_impl.rs @@ -1011,7 +1011,7 @@ mod tests { } context.with_query_results(((Name("John"), Age(42), Weight(220.5)),), &mut |results| { - assert_eq!(results.len(), 1); + assert_eq!(results.into_iter().count(), 1); }); }