diff --git a/docs/book/models/disease_model/src/transmission_manager.rs b/docs/book/models/disease_model/src/transmission_manager.rs index b6646e60..4e3ef223 100644 --- a/docs/book/models/disease_model/src/transmission_manager.rs +++ b/docs/book/models/disease_model/src/transmission_manager.rs @@ -13,7 +13,7 @@ define_rng!(TransmissionRng); fn attempt_infection(context: &mut Context) { trace!("Attempting infection"); let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap(); - let person_status: InfectionStatus = context.get_property(person_to_infect); + let person_status: InfectionStatus = context.get_property::(person_to_infect); if person_status == InfectionStatus::S { context.set_property(person_to_infect, InfectionStatus::I); diff --git a/examples/basic-infection/src/transmission_manager.rs b/examples/basic-infection/src/transmission_manager.rs index bbbb7409..5f9872e1 100644 --- a/examples/basic-infection/src/transmission_manager.rs +++ b/examples/basic-infection/src/transmission_manager.rs @@ -12,7 +12,8 @@ fn attempt_infection(context: &mut Context) { let population_size: usize = context.get_entity_count::(); let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap(); - let person_status: InfectionStatus = context.get_property(person_to_infect); + let person_status: InfectionStatus = + context.get_property::(person_to_infect); if person_status == InfectionStatus::S { context.set_property(person_to_infect, InfectionStatus::I); @@ -54,7 +55,8 @@ mod test { context.init_random(SEED); let person_id = context.add_entity(Person).unwrap(); attempt_infection(&mut context); - let person_status: InfectionStatus = context.get_property(person_id); + let person_status: InfectionStatus = + context.get_property::(person_id); assert_eq!(person_status, InfectionStatus::I); context.execute(); } diff --git a/examples/births-deaths/src/demographics_report.rs b/examples/births-deaths/src/demographics_report.rs index 7cbd82eb..1ed9fd10 100644 --- a/examples/births-deaths/src/demographics_report.rs +++ b/examples/births-deaths/src/demographics_report.rs @@ -21,7 +21,7 @@ define_report!(PersonReportItem); fn handle_person_created(context: &mut Context, event: EntityCreatedEvent) { let person = event.entity_id; - let age_group_person: AgeGroupRisk = context.get_property(person); + let age_group_person: AgeGroupRisk = context.get_property::(person); context.send_report(PersonReportItem { time: context.get_current_time(), person_id: format!("{person}"), @@ -34,7 +34,7 @@ fn handle_person_created(context: &mut Context, event: EntityCreatedEvent) { let person = event.entity_id; - let age_group_person: AgeGroupRisk = context.get_property(person); + let age_group_person: AgeGroupRisk = context.get_property::(person); context.send_report(PersonReportItem { time: context.get_current_time(), person_id: format!("{person}"), @@ -48,7 +48,7 @@ fn handle_person_aging(context: &mut Context, event: PropertyChangeEvent) { if !event.current.0 { let person = event.entity_id; - let age_group_person: AgeGroupRisk = context.get_property(person); + let age_group_person: AgeGroupRisk = context.get_property::(person); context.send_report(PersonReportItem { time: context.get_current_time(), person_id: format!("{person}"), diff --git a/examples/births-deaths/src/incidence_report.rs b/examples/births-deaths/src/incidence_report.rs index 26e3ac72..514f2561 100644 --- a/examples/births-deaths/src/incidence_report.rs +++ b/examples/births-deaths/src/incidence_report.rs @@ -22,8 +22,9 @@ fn handle_infection_status_change( context: &mut Context, event: PropertyChangeEvent, ) { - let age_person: Age = context.get_property(event.entity_id); - let age_group_person: AgeGroupRisk = context.get_property(event.entity_id); + let age_person: Age = context.get_property::(event.entity_id); + let age_group_person: AgeGroupRisk = + context.get_property::(event.entity_id); context.send_report(IncidenceReportItem { time: context.get_current_time(), person_id: format!("{}", event.entity_id), diff --git a/examples/births-deaths/src/infection_manager.rs b/examples/births-deaths/src/infection_manager.rs index 82fc4c38..02fc5a8e 100644 --- a/examples/births-deaths/src/infection_manager.rs +++ b/examples/births-deaths/src/infection_manager.rs @@ -29,7 +29,7 @@ fn schedule_recovery(context: &mut Context, person_id: PersonId) { let infection_duration = parameters.infection_duration; let recovery_time = context.get_current_time() + context.sample_distr(InfectionRng1, Exp::new(1.0 / infection_duration).unwrap()); - let is_alive: Alive = context.get_property(person_id); + let is_alive: Alive = context.get_property::(person_id); if is_alive.0 { let plan_id = context.add_plan(recovery_time, move |context| { diff --git a/examples/births-deaths/src/population_manager.rs b/examples/births-deaths/src/population_manager.rs index 1b2351c4..f98fe5d3 100644 --- a/examples/births-deaths/src/population_manager.rs +++ b/examples/births-deaths/src/population_manager.rs @@ -56,9 +56,9 @@ impl fmt::Display for AgeGroupRisk { } fn schedule_aging(context: &mut Context, person_id: PersonId) { - let is_alive: Alive = context.get_property(person_id); + let is_alive: Alive = context.get_property::(person_id); if is_alive.0 { - let prev_age: Age = context.get_property(person_id); + let prev_age: Age = context.get_property::(person_id); context.set_property(person_id, Age(prev_age.0 + 1)); let next_age_event = context.get_current_time() + 365.0; context.add_plan(next_age_event, move |context| { @@ -168,8 +168,8 @@ mod test { let population = context.get_entity_count::(); // Even if these people have died during simulation, we can still get their properties - let age_0: Age = context.get_property(person1); - let age_1: Age = context.get_property((*person2).borrow().unwrap()); + let age_0: Age = context.get_property::(person1); + let age_1: Age = context.get_property::((*person2).borrow().unwrap()); assert_eq!(age_0.0, 10); assert_eq!(age_1.0, 0); diff --git a/examples/births-deaths/src/transmission_manager.rs b/examples/births-deaths/src/transmission_manager.rs index 3fdb4c13..f1a131c8 100644 --- a/examples/births-deaths/src/transmission_manager.rs +++ b/examples/births-deaths/src/transmission_manager.rs @@ -22,7 +22,8 @@ fn attempt_infection(context: &mut Context, age_group: AgeGroupRisk) { .get(&age_group) .unwrap(); - let person_status: InfectionStatus = context.get_property(person_to_infect); + let person_status: InfectionStatus = + context.get_property::(person_to_infect); if person_status == InfectionStatus::S { context.set_property(person_to_infect, InfectionStatus::I); diff --git a/examples/network-hhmodel/incidence_report.rs b/examples/network-hhmodel/incidence_report.rs index 874d9232..09c6bc20 100644 --- a/examples/network-hhmodel/incidence_report.rs +++ b/examples/network-hhmodel/incidence_report.rs @@ -28,7 +28,7 @@ fn handle_infection_status_change( } // figure out who infected whom - let infected_by: InfectedBy = context.get_property(event.entity_id); + let infected_by: InfectedBy = context.get_property::(event.entity_id); let infected_by_val = match infected_by.0 { None => "NA".to_string(), Some(id) => id.to_string(), @@ -96,7 +96,7 @@ mod test { return; } - let infected_by: InfectedBy = context.get_property(event.entity_id); + let infected_by: InfectedBy = context.get_property::(event.entity_id); let infected_by_val = match infected_by.0 { None => "NA".to_string(), Some(id) => id.to_string(), diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index e9bd7061..35d895ec 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -19,7 +19,7 @@ struct EdgeRecord { fn create_household_networks(context: &mut Context, people: &[PersonId]) { let mut households = HashSet::new(); for person_id in people { - let household_id: HouseholdId = context.get_property(*person_id); + let household_id: HouseholdId = context.get_property::(*person_id); if households.insert(household_id) { let mut members: Vec = Vec::new(); context.with_query_results(with!(Person, household_id), &mut |results| { diff --git a/integration-tests/ixa-runner-tests/tests/macros.rs b/integration-tests/ixa-runner-tests/tests/macros.rs index f3df75c3..a99b29f0 100644 --- a/integration-tests/ixa-runner-tests/tests/macros.rs +++ b/integration-tests/ixa-runner-tests/tests/macros.rs @@ -156,14 +156,14 @@ mod tests { TestPropOpt(Some(3u8)), )) .unwrap(); - let val: TestPropU32 = ctx.get_property(pid); + let val: TestPropU32 = ctx.get_property::(pid); assert_eq!(val.0, 10u32); // Verify default property value is set for TestPropDefault - let default_val: TestPropDefault = ctx.get_property(pid); + let default_val: TestPropDefault = ctx.get_property::(pid); assert_eq!(default_val.0, 7u32); // Derived property should compute from TestPropU32 - let d: DerivedProp = ctx.get_property(pid); + let d: DerivedProp = ctx.get_property::(pid); assert_eq!(d.0, 11u32); // Derived property `impl_eq_hash = ...` variants should all compile and behave as hashable keys. diff --git a/integration-tests/ixa-wasm-tests/src/transmission_manager.rs b/integration-tests/ixa-wasm-tests/src/transmission_manager.rs index b7513922..eaf7ff77 100644 --- a/integration-tests/ixa-wasm-tests/src/transmission_manager.rs +++ b/integration-tests/ixa-wasm-tests/src/transmission_manager.rs @@ -12,7 +12,8 @@ fn attempt_infection(context: &mut Context) { let population_size: usize = context.get_entity_count::(); let person_to_infect: PersonId = context.sample_entity(TransmissionRng, Person).unwrap(); //.sample_range(TransmissionRng, 0..population_size); - let person_status: InfectionStatus = context.get_property(person_to_infect); + let person_status: InfectionStatus = + context.get_property::(person_to_infect); if person_status == InfectionStatus::S { context.set_property(person_to_infect, InfectionStatus::I); @@ -54,7 +55,8 @@ mod test { context.init_random(SEED); let person_id: PersonId = context.add_entity(Person).unwrap(); attempt_infection(&mut context); - let person_status: InfectionStatus = context.get_property(person_id); + let person_status: InfectionStatus = + context.get_property::(person_id); assert_eq!(person_status, InfectionStatus::I); context.execute(); } diff --git a/src/entity/context_extension.rs b/src/entity/context_extension.rs index af9189ce..165ba485 100644 --- a/src/entity/context_extension.rs +++ b/src/entity/context_extension.rs @@ -102,11 +102,14 @@ pub trait ContextEntitiesExt { /// Fetches the property value set for the given `entity_id`. /// - /// The easiest way to call this method is by assigning it to a variable with an explicit type: + /// Returns `P::Value` — for legacy newtype properties this is `Self` (the wrapper), + /// for primitive-form properties this is the inner primitive type. + /// + /// The recommended call style is to name the entity and property explicitly: /// ```rust, ignore - /// let vaccine_status: VaccineStatus = context.get_property(entity_id); + /// let vaccine_status = context.get_property::(entity_id); /// ``` - fn get_property>(&self, entity_id: EntityId) -> P; + fn get_property>(&self, entity_id: EntityId) -> P::Value; /// Sets the value of the given property. This method unconditionally emits a `PropertyChangeEvent`. fn set_property>( @@ -270,12 +273,12 @@ impl ContextEntitiesExt for Context { Ok(new_entity_id) } - fn get_property>(&self, entity_id: EntityId) -> P { + fn get_property>(&self, entity_id: EntityId) -> P::Value { if P::is_derived() { - P::compute_derived(self, entity_id) + P::compute_derived(self, entity_id).into_value() } else { let property_store = self.get_property_value_store::(); - property_store.get(entity_id) + property_store.get(entity_id).into_value() } } @@ -839,11 +842,12 @@ mod tests { let person = context.add_entity(with!(Person, Age(25))).unwrap(); // Retrieve and check their values - let age: Age = context.get_property(person); + let age: Age = context.get_property::(person); assert_eq!(age, Age(25)); - let infection_status: InfectionStatus = context.get_property(person); + let infection_status: InfectionStatus = + context.get_property::(person); assert_eq!(infection_status, InfectionStatus::Susceptible); - let vaccinated: Vaccinated = context.get_property(person); + let vaccinated: Vaccinated = context.get_property::(person); assert_eq!(vaccinated, Vaccinated(false)); // Change them @@ -852,11 +856,12 @@ mod tests { context.set_property(person, Vaccinated(true)); // Retrieve and check their values - let age: Age = context.get_property(person); + let age: Age = context.get_property::(person); assert_eq!(age, Age(26)); - let infection_status: InfectionStatus = context.get_property(person); + let infection_status: InfectionStatus = + context.get_property::(person); assert_eq!(infection_status, InfectionStatus::Infected); - let vaccinated: Vaccinated = context.get_property(person); + let vaccinated: Vaccinated = context.get_property::(person); assert_eq!(vaccinated, Vaccinated(true)); } @@ -875,11 +880,12 @@ mod tests { .unwrap(); // Retrieve and check their values - let age: Age = context.get_property(person); + let age: Age = context.get_property::(person); assert_eq!(age, Age(25)); - let infection_status: InfectionStatus = context.get_property(person); + let infection_status: InfectionStatus = + context.get_property::(person); assert_eq!(infection_status, InfectionStatus::Recovered); - let vaccinated: Vaccinated = context.get_property(person); + let vaccinated: Vaccinated = context.get_property::(person); assert_eq!(vaccinated, Vaccinated(true)); // Change them @@ -888,11 +894,12 @@ mod tests { context.set_property(person, Vaccinated(false)); // Retrieve and check their values - let age: Age = context.get_property(person); + let age: Age = context.get_property::(person); assert_eq!(age, Age(26)); - let infection_status: InfectionStatus = context.get_property(person); + let infection_status: InfectionStatus = + context.get_property::(person); assert_eq!(infection_status, InfectionStatus::Infected); - let vaccinated: Vaccinated = context.get_property(person); + let vaccinated: Vaccinated = context.get_property::(person); assert_eq!(vaccinated, Vaccinated(false)); } @@ -982,11 +989,11 @@ mod tests { )) .unwrap(); - let actual_high: RiskLevel = context.get_property(expected_high_id); + let actual_high: RiskLevel = context.get_property::(expected_high_id); assert_eq!(actual_high, RiskLevel::High); - let actual_med: RiskLevel = context.get_property(expected_med_id); + let actual_med: RiskLevel = context.get_property::(expected_med_id); assert_eq!(actual_med, RiskLevel::Medium); - let actual_low: RiskLevel = context.get_property(expected_low_id); + let actual_low: RiskLevel = context.get_property::(expected_low_id); assert_eq!(actual_low, RiskLevel::Low); } @@ -1066,7 +1073,7 @@ mod tests { .add_entity(with!(Person, Age(17), IsSwimmer(true))) .unwrap(); - let is_adult_athlete: AdultAthlete = context.get_property(person); + let is_adult_athlete: AdultAthlete = context.get_property::(person); assert!(!is_adult_athlete.0); let flag = Rc::new(RefCell::new(0)); @@ -1082,7 +1089,7 @@ mod tests { context.set_property(person, Age(20)); // Make sure the derived property is what we expect. - let is_adult_athlete: AdultAthlete = context.get_property(person); + let is_adult_athlete: AdultAthlete = context.get_property::(person); assert!(is_adult_athlete.0); // Execute queued event handlers diff --git a/src/entity/events.rs b/src/entity/events.rs index 59ef283d..44c41f3b 100644 --- a/src/entity/events.rs +++ b/src/entity/events.rs @@ -40,8 +40,8 @@ use crate::{Context, IxaEvent}; // `#[repr(transparent)]` over `PropertyChangeEvent`. That event stores // // - `EntityId`, one `usize`, so 8 bytes. -// - `current`: a property value, typically <= 8 bytes -// - `current`: a property value, typically <= 8 bytes +// - `current`: `P::Value`, typically <= 8 bytes +// - `previous`: `P::Value`, typically <= 8 bytes // // That puts the payload at 24 bytes, with 8-byte alignment. The `S4` size is 4 `usize`s of inline // storage, i.e. 32 bytes, and inline storage is used when the payload size and alignment fit. So @@ -60,16 +60,18 @@ impl> PartialPropertyChangeEvent { /// Updates the index with the current property value and emits a change event. fn emit_in_context(&mut self, context: &mut Context) { - self.0.current = context.get_property(self.0.entity_id); + self.0.current = context.get_property::(self.0.entity_id); { // Update value change counters let property_value_store = context.get_property_value_store::(); if self.0.current != self.0.previous { for counter in &property_value_store.value_change_counters { - counter - .borrow_mut() - .update(self.0.entity_id, self.0.current, context); + counter.borrow_mut().update( + self.0.entity_id, + P::from_value(self.0.current), + context, + ); } } } @@ -77,13 +79,15 @@ impl> PartialPropertyChangeEvent // Now update the indexes let property_value_store = context.get_property_value_store_mut::(); // Out with the old - property_value_store - .index - .remove_entity(&self.0.previous.make_canonical(), self.0.entity_id); + property_value_store.index.remove_entity( + &P::from_value(self.0.previous).make_canonical(), + self.0.entity_id, + ); // In with the new - property_value_store - .index - .add_entity(&self.0.current.make_canonical(), self.0.entity_id); + property_value_store.index.add_entity( + &P::from_value(self.0.current).make_canonical(), + self.0.entity_id, + ); // We decided not to do the following check. // See `src/entity/context_extension::ContextEntitiesExt::set_property`. @@ -115,7 +119,7 @@ impl> Clone for PartialPropertyChangeEventCore { impl> Copy for PartialPropertyChangeEventCore {} impl> PartialPropertyChangeEventCore { - pub fn new(entity_id: EntityId, previous_value: P) -> Self { + pub fn new(entity_id: EntityId, previous_value:

>::Value) -> Self { Self(PropertyChangeEvent { entity_id, current: previous_value, @@ -153,15 +157,18 @@ impl EntityCreatedEvent { /// Emitted when a property is updated. /// These should not be emitted outside this module. +/// +/// `current` and `previous` are `P::Value`: for legacy newtype properties this is the wrapper +/// (unchanged behavior); for primitive-form properties this is the inner primitive type. #[derive(IxaEvent)] #[allow(clippy::manual_non_exhaustive)] pub struct PropertyChangeEvent> { /// The [`EntityId`] that changed pub entity_id: EntityId, /// The new value - pub current: P, + pub current:

>::Value, /// The old value - pub previous: P, + pub previous:

>::Value, } // We provide blanket impls for these because the compiler isn't smart enough to know // this type is always `Copy`/`Clone` if we derive them. diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 070a90a5..f1a8db7a 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -14,7 +14,7 @@ Entity property getters and setters exists on `Context` like this: // (The `MyProperty` type knows which entity it belongs to.) let my_property_value = context.get_property::(my_entity_id); // Turbofish-less version of the same call: -let my_property_value: MyProperty = context.get_property(my_entity_id); +let my_property_value: MyProperty = context.get_property::(my_entity_id); // For setters, the property is inferred from the type of the value we are passing in. context.set_property(my_entity_id, some_property_value); diff --git a/src/entity/property.rs b/src/entity/property.rs index 81983a46..42f0ba1a 100644 --- a/src/entity/property.rs +++ b/src/entity/property.rs @@ -63,6 +63,22 @@ pub const fn const_str_eq(a: &str, b: &str) -> bool { /// Property values and canonical values must satisfy `AnyProperty` so they can participate in /// property indexes. pub trait Property: AnyProperty { + /// The "user-facing" value type of a property. For complex newtype properties this is `Self` + /// (the wrapper itself); for primitive-form properties defined as `Name: Primitive` this is + /// the inner primitive type. `get_property` returns `Self::Value`. + type Value: AnyProperty; + + /// Extracts the `Value` from a property. For complex properties this is the identity; for + /// primitive-form properties it unwraps the inner field. + #[must_use] + fn into_value(self) -> Self::Value; + + /// Wraps a `Value` back into the property type. The inverse of [`into_value`]. For complex + /// properties this is the identity; for primitive-form properties it wraps the primitive in + /// the newtype. + #[must_use] + fn from_value(value: Self::Value) -> Self; + /// Some properties might store a transformed version of the value in the index. This is the /// type of the transformed value. For simple properties this will be the same as `Self`. type CanonicalValue: AnyProperty; diff --git a/src/entity/property_list.rs b/src/entity/property_list.rs index f3bcc611..8b888df1 100644 --- a/src/entity/property_list.rs +++ b/src/entity/property_list.rs @@ -137,7 +137,7 @@ impl> PropertyList for (P,) { } fn get_values_for_entity(context: &Context, entity_id: EntityId) -> Self { - (context.get_property::(entity_id),) + (P::from_value(context.get_property::(entity_id)),) } } @@ -178,7 +178,7 @@ macro_rules! impl_property_list { } fn get_values_for_entity(context: &Context, entity_id: EntityId) -> Self { - (#(context.get_property::(entity_id), )*) + (#(P~N::from_value(context.get_property::(entity_id)), )*) } } }); diff --git a/src/entity/property_value_store.rs b/src/entity/property_value_store.rs index 91c7e44a..b596315d 100644 --- a/src/entity/property_value_store.rs +++ b/src/entity/property_value_store.rs @@ -89,7 +89,7 @@ impl> PropertyValueStore for PropertyValueStoreCore smallbox::smallbox!(PartialPropertyChangeEventCore::::new( entity_id, - previous_value, + previous_value.into_value(), )) } diff --git a/src/entity/query/query_impls.rs b/src/entity/query/query_impls.rs index 89894113..1c6aec82 100644 --- a/src/entity/query/query_impls.rs +++ b/src/entity/query/query_impls.rs @@ -184,7 +184,7 @@ impl> QueryInternal for (P1,) { } fn match_entity(&self, entity_id: EntityId, context: &Context) -> bool { - let found_value: P1 = context.get_property(entity_id); + let found_value: P1 = P1::from_value(context.get_property::(entity_id)); found_value == self.0 } @@ -309,7 +309,7 @@ macro_rules! impl_query { fn match_entity(&self, entity_id: EntityId, context: &Context) -> bool { #( { - let found_value: T~N = context.get_property(entity_id); + let found_value: T~N = T~N::from_value(context.get_property::(entity_id)); if found_value != self.N { return false } diff --git a/src/macros/property_impl.rs b/src/macros/property_impl.rs index e7d47bdb..1421c276 100644 --- a/src/macros/property_impl.rs +++ b/src/macros/property_impl.rs @@ -227,6 +227,72 @@ macro_rules! define_property { // `impl_derived_property!(@with_option_display_default ...)` and // `impl_property!(@with_option_display_default ...)` subcommands. + // Primitive form: `define_property!(Age: u8, Person)`. + // + // Generates a newtype `pub struct Age(pub u8)` plus `From for Age` and + // `From for u8`, then dispatches to `impl_property!` with + // `value_type = u8` and `into_value_fn = |Age(v)| v`. The newtype is the + // identity of the property (used by `set_property`, `with!`, indexes, and + // events, exactly like the legacy newtype form), while `Property::Value` + // is the inner primitive — so `get_property::<_, Age>(pid)` returns `u8`. + ( + $name:ident : $value_ty:ty, + $entity:ident, + impl_eq_hash = $impl_eq_hash:ident + $(, $($extra:tt)*)? + ) => { + $crate::define_property!( + @apply_property_decoration $impl_eq_hash, + pub struct $name(pub $value_ty);, + $name + ); + $crate::define_property!(@primitive_conversions $name, $value_ty); + $crate::impl_property!( + $name, + $entity + $(, $($extra)*)?, + value_type = $value_ty, + into_value_fn = |v: $name| v.0, + from_value_fn = |v: $value_ty| $name(v) + ); + }; + ( + $name:ident : $value_ty:ty, + $entity:ident + $(, $($extra:tt)*)? + ) => { + $crate::define_property!( + @apply_property_decoration , + pub struct $name(pub $value_ty);, + $name + ); + $crate::define_property!(@primitive_conversions $name, $value_ty); + $crate::impl_property!( + $name, + $entity + $(, $($extra)*)?, + value_type = $value_ty, + into_value_fn = |v: $name| v.0, + from_value_fn = |v: $value_ty| $name(v) + ); + }; + + // Helper: generate `From<$value_ty>` for the wrapper and `From<$name>` for the inner type. + (@primitive_conversions $name:ident, $value_ty:ty) => { + impl ::core::convert::From<$value_ty> for $name { + #[inline] + fn from(value: $value_ty) -> Self { + $name(value) + } + } + impl ::core::convert::From<$name> for $value_ty { + #[inline] + fn from(value: $name) -> Self { + value.0 + } + } + }; + ( struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ), $entity:ident, @@ -479,6 +545,9 @@ macro_rules! impl_property { $(, collect_deps_fn = $collect_deps_fn:expr)? $(, display_impl = $display_impl:expr)? $(, ctor_registration = $ctor_registration:expr)? + $(, value_type = $value_type:ty)? + $(, into_value_fn = $into_value_fn:expr)? + $(, from_value_fn = $from_value_fn:expr)? ) => { // Enforce mutual exclusivity at compile time. $crate::impl_property!(@assert_not_both $($compute_derived_fn)? ; $($default_const)?); @@ -488,6 +557,15 @@ macro_rules! impl_property { $property, $entity, + // value type (defaults to Self for the legacy newtype form) + $crate::impl_property!(@unwrap_or_ty $($value_type)?, $property), + + // into_value_fn (defaults to identity; primitive form overrides to unwrap `.0`) + $crate::impl_property!(@unwrap_or $($into_value_fn)?, std::convert::identity), + + // from_value_fn (defaults to identity; primitive form overrides to wrap with `Self(...)`) + $crate::impl_property!(@unwrap_or $($from_value_fn)?, std::convert::identity), + // canonical value $crate::impl_property!(@unwrap_or_ty $($canonical_value)?, $property), @@ -573,6 +651,9 @@ macro_rules! impl_property { $(, collect_deps_fn = $collect_deps_fn:expr)? $(, display_impl = $display_impl:expr)? $(, ctor_registration = $ctor_registration:expr)? + $(, value_type = $value_type:ty)? + $(, into_value_fn = $into_value_fn:expr)? + $(, from_value_fn = $from_value_fn:expr)? ) => { $crate::impl_property!( $property, @@ -591,6 +672,9 @@ macro_rules! impl_property { } }) $(, ctor_registration = $ctor_registration)? + $(, value_type = $value_type)? + $(, into_value_fn = $into_value_fn)? + $(, from_value_fn = $from_value_fn)? ); }; @@ -615,6 +699,11 @@ macro_rules! impl_property { @__impl_property_common $property, $entity, + // Multi-properties stay Value = Self; the tuple itself is both the value + // type and the property identity. + $property, + std::convert::identity, + std::convert::identity, $crate::impl_property!(@unwrap_or_ty $($canonical_value)?, $property), $crate::impl_property!(@select_initialization_kind $($compute_derived_fn)? ; $($default_const)?), $crate::impl_property!( @@ -712,6 +801,9 @@ macro_rules! impl_property { @__impl_property_common $property:ident, // The name of the type we are implementing `Property` for $entity:ident, // The entity type this property is associated with + $value_type:ty, // The `Property::Value` associated type. Defaults to `Self` for the legacy newtype form; primitive form sets this to the inner primitive type. + $into_value_fn:expr, // A function `Self -> Self::Value`. Defaults to `identity` for the legacy form; primitive form sets this to `|s| s.0`. + $from_value_fn:expr, // A function `Self::Value -> Self`. Defaults to `identity`; primitive form sets this to `|v| Self(v)`. $canonical_value:ty, // If the type stored in the index is different from Self, the name of that type $initialization_kind:expr, // The kind of initialization this property has (implicit selection) $compute_derived_fn:expr, // If the property is derived, the function that computes the value @@ -727,11 +819,20 @@ macro_rules! impl_property { $ctor_registration:expr, // Code that runs in a ctor for property registration ) => { impl $crate::entity::property::Property<$entity> for $property { + type Value = $value_type; type CanonicalValue = $canonical_value; type QueryParts<'a> = $query_parts_type where Self: 'a; const NAME: &'static str = stringify!($property); + fn into_value(self) -> Self::Value { + ($into_value_fn)(self) + } + + fn from_value(value: Self::Value) -> Self { + ($from_value_fn)(value) + } + fn initialization_kind() -> $crate::entity::property::PropertyInitializationKind { $initialization_kind } @@ -744,7 +845,11 @@ macro_rules! impl_property { } fn default_const() -> Self { - $default_const + // `$default_const` is the inner `Self::Value`; wrap it back into `Self`. For + // legacy newtype properties this is the identity. When no default was provided + // it expands to `panic!(...)`, so silence the unreachable-call lint. + #[allow(unreachable_code)] + >::from_value($default_const) } fn make_canonical(self) -> Self::CanonicalValue { @@ -819,6 +924,20 @@ macro_rules! impl_property { /// * Optional parameters: The same optional parameters accepted by [`impl_property!`][macro@crate::impl_property], /// plus `impl_eq_hash = Eq | Hash | both | neither` to control whether `Eq`/`Hash` are derived or generated /// for the declared type, mirroring [`define_property!`][macro@crate::define_property]. +/// +/// The primitive form `define_derived_property!(IsAdult: bool, Person, [Age], |age| IsAdult(age >= 18))` +/// does NOT accept trailing keyword extras like `display_impl = ...`. The compile error is +/// guided: +/// +/// ```compile_fail +/// use ixa::{define_entity, define_property, define_derived_property}; +/// define_entity!(Person); +/// define_property!(Age: u8, Person, default_const = 0); +/// define_derived_property!( +/// IsAdult: bool, Person, [Age], |age| IsAdult(age >= 18), +/// display_impl = |v: &IsAdult| format!("{:?}", v.0) +/// ); +/// ``` #[macro_export] macro_rules! define_derived_property { // Implementation Notes @@ -828,6 +947,108 @@ macro_rules! define_derived_property { // // See `derive_property!` implementation notes for why each type form is duplicated. + // Primitive form: `define_derived_property!(IsAdult: bool, Person, [Age], |age| IsAdult(...))`. + // + // Same generation strategy as the `define_property!` primitive arm: a newtype wrapper + // `pub struct IsAdult(pub bool)`, From impls in both directions, then `impl_property!` + // directly (bypassing `impl_derived_property!`) so we can thread `value_type = bool` past the + // ambiguous `$($extra:tt)+` pattern in `impl_derived_property!`. The derive closure still + // returns `Self` (the wrapper), matching the legacy form. + // + // The primitive arms do NOT accept trailing keyword extras (e.g. `display_impl = ...`) so the + // dispatch is unambiguous. Users who need those should use the legacy `struct` form. + ( + $name:ident : $value_ty:ty, + $entity:ident, + [$($dependency:ident),*] + $(, [$($global_dependency:ident),*])?, + |$($param:ident),+| $derive_fn:expr, + impl_eq_hash = $impl_eq_hash:ident + ) => { + $crate::define_property!( + @apply_property_decoration + $impl_eq_hash, + pub struct $name(pub $value_ty);, + $name + ); + $crate::define_property!(@primitive_conversions $name, $value_ty); + $crate::define_derived_property!( + @__primitive_impl + $name, $entity, $value_ty, + [$($dependency),*], [$($($global_dependency),*)?], + |$($param),+| $derive_fn + ); + }; + ( + $name:ident : $value_ty:ty, + $entity:ident, + [$($dependency:ident),*] + $(, [$($global_dependency:ident),*])?, + |$($param:ident),+| $derive_fn:expr + ) => { + $crate::define_property!( + @apply_property_decoration + , + pub struct $name(pub $value_ty);, + $name + ); + $crate::define_property!(@primitive_conversions $name, $value_ty); + $crate::define_derived_property!( + @__primitive_impl + $name, $entity, $value_ty, + [$($dependency),*], [$($($global_dependency),*)?], + |$($param),+| $derive_fn + ); + }; + + // Diagnostic arm: catch primitive-form invocations that pass trailing keyword extras + // (e.g. `display_impl = ...`) and surface a clear error pointing users at the legacy + // `struct` form. Without this arm they'd get a generic "no rules matched" error. + ( + $name:ident : $value_ty:ty, + $entity:ident, + [$($dependency:ident),*] + $(, [$($global_dependency:ident),*])?, + |$($param:ident),+| $derive_fn:expr, + $($extra:tt)+ + ) => { + compile_error!(concat!( + "define_derived_property!(", stringify!($name), ": ", stringify!($value_ty), + ", ...) does not accept trailing keyword arguments other than `impl_eq_hash`. ", + "Use the legacy form `define_derived_property!(struct ", stringify!($name), + "(", stringify!($value_ty), "), ...)` if you need `display_impl`, ", + "`default_const`, `canonical_value`, etc." + )); + }; + + // Internal: build the `impl_property!` invocation for a primitive derived property. + // Goes directly to `impl_property!` (skipping `impl_derived_property!`) so the + // `value_type` / `into_value_fn` / `from_value_fn` keyword args are unambiguous. + ( + @__primitive_impl + $name:ident, $entity:ident, $value_ty:ty, + [$($dependency:ident),*], [$($global_dependency:ident),*], + |$($param:ident),+| $derive_fn:expr + ) => { + $crate::impl_property!( + $name, + $entity, + compute_derived_fn = $crate::impl_derived_property!( + @construct_compute_fn + $entity, + [$($dependency),*], + [$($global_dependency),*], + |$($param),+| $derive_fn + ), + collect_deps_fn = $crate::impl_derived_property!( + @construct_collect_deps_fn $entity, [$($dependency),*] + ), + value_type = $value_ty, + into_value_fn = |v: $name| v.0, + from_value_fn = |v: $value_ty| $name(v) + ); + }; + // Struct (tuple) with single Option field ( struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ), @@ -1068,19 +1289,32 @@ macro_rules! impl_derived_property { [$($($global_dependency),*)?], |$($param),+| $derive_fn ), - collect_deps_fn = | deps: &mut $crate::HashSet | { - $( - if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() { - <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps); - } else { - deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id()); - } - )* - } + collect_deps_fn = $crate::impl_derived_property!( + @construct_collect_deps_fn $entity, [$($dependency),*] + ) $(, $($extra)+)* ); }; + // Internal branch: build the `collect_deps_fn` closure that inserts each non-derived + // dependency's id (or, if the dependency is itself derived, recursively collects its + // non-derived dependencies). + ( + @construct_collect_deps_fn + $entity:ident, + [$($dependency:ident),*] + ) => { + |deps: &mut $crate::HashSet| { + $( + if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() { + <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps); + } else { + deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id()); + } + )* + } + }; + // Internal branch to construct the compute function. ( @construct_compute_fn @@ -1195,8 +1429,15 @@ macro_rules! define_multi_property { $entity, ( $($dependency),+ ), compute_derived_fn = |context: &$crate::Context, entity_id: $crate::entity::EntityId<$entity>| { + // Wrap each dep back into its property type via `from_value`. For legacy + // newtype deps this is identity; for primitive-form deps it re-wraps the + // inner value so the tuple matches `Self = (Dep1, Dep2, ...)`. ( - $(context.get_property::<$entity, $dependency>(entity_id)),+ + $( + <$dependency as $crate::entity::property::Property<$entity>>::from_value( + context.get_property::<$entity, $dependency>(entity_id) + ) + ),+ ) }, canonical_value = $crate::sorted_tag!(( $($dependency),+ )), @@ -1537,9 +1778,9 @@ mod tests { .add_entity::(with!(Person, 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); + 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); @@ -1713,4 +1954,141 @@ mod tests { values.insert(DerivedWeight(3.0)); assert!(values.contains(&DerivedWeight(3.0))); } + + // ---- Primitive-form `define_property!(Name: Type, Entity)` tests ---- + // + // The generated newtype keeps `set_property` / `with!` ergonomics working with the wrapper, + // while `get_property` returns the inner primitive (`P::Value`). + + define_property!(MyAge: u8, Person, default_const = 0); + define_property!(Score: i32, Person, default_const = 0); + define_property!(StepCount: u32, Person, default_const = 100); + + define_derived_property!( + IsAdult: bool, + Person, + [MyAge], + |age| IsAdult(age >= 18) + ); + + #[test] + fn test_primitive_property_get_returns_inner() { + let mut context = Context::new(); + let person = context.add_entity(with!(Person, MyAge(33))).unwrap(); + + // `get_property` returns `u8`, not `MyAge`. + let age: u8 = context.get_property::(person); + assert_eq!(age, 33); + + // The wrapper still works for `set_property` and `with!`. + context.set_property(person, MyAge(50)); + let age: u8 = context.get_property::(person); + assert_eq!(age, 50); + + // Generated From impls are bidirectional. + let wrapped: MyAge = 7u8.into(); + assert_eq!(wrapped, MyAge(7)); + let unwrapped: u8 = MyAge(9).into(); + assert_eq!(unwrapped, 9); + + // into_value and from_value round-trip. + assert_eq!(MyAge(11).into_value(), 11u8); + assert_eq!(MyAge::from_value(13u8), MyAge(13)); + } + + #[test] + fn test_primitive_derived_property() { + let mut context = Context::new(); + let kid = context.add_entity(with!(Person, MyAge(12))).unwrap(); + let grownup = context.add_entity(with!(Person, MyAge(30))).unwrap(); + + // The derived primitive returns `bool` from `get_property`. + let is_adult_kid: bool = context.get_property::(kid); + let is_adult_grownup: bool = context.get_property::(grownup); + assert!(!is_adult_kid); + assert!(is_adult_grownup); + + // Mutating the dependency re-derives. + context.set_property(kid, MyAge(40)); + assert!(context.get_property::(kid)); + } + + #[test] + fn test_primitive_default_const_accepts_inner_value() { + // `default_const = 100` (the bare `u32`) should produce a `StepCount(100)` default. + let mut context = Context::new(); + let person = context.add_entity(Person).unwrap(); + assert_eq!(context.get_property::(person), 100); + } + + #[test] + fn test_primitive_property_signed_int() { + let mut context = Context::new(); + let person = context.add_entity(with!(Person, Score(-7))).unwrap(); + let score: i32 = context.get_property::(person); + assert_eq!(score, -7); + } + + #[test] + fn test_primitive_property_indexed_query() { + // Exercises the `from_value` round-trip inside `query_impls.rs::match_entity` and the + // canonical-value index path for a primitive property. + let mut context = Context::new(); + context.index_property::(); + + for age in [10u8, 20, 20, 30] { + context.add_entity(with!(Person, MyAge(age))).unwrap(); + } + + assert_eq!(context.query_entity_count(with!(Person, MyAge(20))), 2); + assert_eq!(context.query_entity_count(with!(Person, MyAge(10))), 1); + assert_eq!(context.query_entity_count(with!(Person, MyAge(99))), 0); + } + + #[test] + fn test_primitive_property_change_event() { + // Events expose `P::Value`: for primitive-form properties subscribers see the inner + // primitive directly, not the wrapper. + use std::cell::RefCell; + use std::rc::Rc; + + let mut context = Context::new(); + let person = context.add_entity(with!(Person, MyAge(10))).unwrap(); + + let observed: Rc>> = Rc::new(RefCell::new(None)); + let observed_clone = Rc::clone(&observed); + context.subscribe_to_event(move |_ctx, event: PropertyChangeEvent| { + *observed_clone.borrow_mut() = Some((event.previous, event.current)); + }); + + context.set_property(person, MyAge(25)); + context.execute(); + + let (prev, curr) = observed.borrow().expect("event should have fired"); + assert_eq!(prev, 10u8); + assert_eq!(curr, 25u8); + } + + define_multi_property!((Name, MyAge), Person); + + #[test] + fn test_primitive_property_in_multi_property() { + // `MyAge: u8` is a primitive-form property; the multi-property compute closure must + // wrap it back into `MyAge` so the tuple matches `Self = (Name, MyAge)`. + let mut context = Context::new(); + context + .add_entity(with!(Person, Name("alice"), MyAge(5))) + .unwrap(); + context + .add_entity(with!(Person, Name("alice"), MyAge(7))) + .unwrap(); + assert_eq!( + context.query_entity_count(with!(Person, Name("alice"), MyAge(5))), + 1 + ); + assert_eq!( + context.query_entity_count(with!(Person, Name("alice"), MyAge(7))), + 1 + ); + } }