From d81363b7f9cd1b89c58ba3638f3c0d1c85b69f3e Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 20:32:30 -0500 Subject: [PATCH 1/8] feat: rename all to q --- src/entity/query/mod.rs | 34 ++++++++++++++++----------------- src/macros/mod.rs | 2 +- src/macros/{all.rs => q.rs} | 38 +++++++++++++++++-------------------- 3 files changed, 35 insertions(+), 39 deletions(-) rename src/macros/{all.rs => q.rs} (70%) diff --git a/src/entity/query/mod.rs b/src/entity/query/mod.rs index 260aac3b..44c9b07f 100644 --- a/src/entity/query/mod.rs +++ b/src/entity/query/mod.rs @@ -15,7 +15,7 @@ use crate::{Context, IxaError}; /// A newtype wrapper that associates a tuple of property values with an entity type. /// -/// This is not meant to be used directly, but rather as a backing for the all! macro/ +/// This is not meant to be used directly, but rather as a backing for the q! macro/ /// a replacement for the query tuple. /// /// # Example @@ -26,7 +26,7 @@ use crate::{Context, IxaError}; /// define_property!(struct Age(u8), Person, default_const = Age(0)); /// /// // Use the all macro -/// let query = all!(Person, Age(42)); +/// let query = q!(Person, Age(42)); /// // Under the hood this is: /// // EntityPropertyTuple::::new((Age(42),)); /// ``` @@ -627,42 +627,42 @@ mod tests { #[test] fn all_macro_no_properties() { - use crate::all; + use crate::q; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::Low)).unwrap(); - // all!(Person) should match all Person entities - let query = all!(Person); + // q!(Person) should match all Person entities + let query = q!(Person); assert_eq!(context.query_entity_count(query), 2); } #[test] fn all_macro_single_property() { - use crate::all; + use crate::q; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap(); - // all!(Person, Age(42)) should match entities with Age = 42 - let query = all!(Person, Age(42)); + // q!(Person, Age(42)) should match entities with Age = 42 + let query = q!(Person, Age(42)); assert_eq!(context.query_entity_count(query), 2); } #[test] fn all_macro_multiple_properties() { - use crate::all; + use crate::q; let mut context = Context::new(); let p1 = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap(); - // all!(Person, Age(42), RiskCategory::High) should match one entity - let query = all!(Person, Age(42), RiskCategory::High); + // q!(Person, Age(42), RiskCategory::High) should match one entity + let query = q!(Person, Age(42), RiskCategory::High); assert_eq!(context.query_entity_count(query), 1); context.with_query_results(query, &mut |people| { @@ -672,16 +672,16 @@ mod tests { #[test] fn all_macro_with_trailing_comma() { - use crate::all; + use crate::q; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); // Trailing comma should work - let query = all!(Person, Age(42),); + let query = q!(Person, Age(42)); assert_eq!(context.query_entity_count(query), 1); - let query = all!(Person, Age(42), RiskCategory::High,); + let query = q!(Person, Age(42), RiskCategory::High); assert_eq!(context.query_entity_count(query), 1); } @@ -713,12 +713,12 @@ mod tests { #[test] fn all_macro_as_property_list_for_add_entity() { - use crate::all; + use crate::q; let mut context = Context::new(); - // Use all! macro result to add an entity - let props = all!(Person, Age(42), RiskCategory::High); + // Use q! macro result to add an entity + let props = q!(Person, Age(42), RiskCategory::High); let person = context.add_entity(props).unwrap(); // Verify the entity was created with the correct properties diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 17815366..01d38f6c 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -1,4 +1,3 @@ -mod all; mod assert_almost_eq; mod define_data_plugin; mod define_global_property; @@ -7,3 +6,4 @@ mod define_rng; mod edge_impl; mod entity_impl; mod property_impl; +mod q; diff --git a/src/macros/all.rs b/src/macros/q.rs similarity index 70% rename from src/macros/all.rs rename to src/macros/q.rs index 8ae6c242..e474be54 100644 --- a/src/macros/all.rs +++ b/src/macros/q.rs @@ -4,18 +4,18 @@ /// /// ```ignore /// // Add an entity with default properties -/// let query = all!(Person); +/// let query = q!(Person); /// context.add_entity(query)?; /// /// // An inline query matching a single property -/// let person = context.sample_entity(MyRng, all!(Person, Age(12)))?; +/// let person = context.sample_entity(MyRng, q!(Person, Age(12)))?; /// /// // A query matching multiple properties -/// let query = all!(Person, Age(12), RiskCategory::High); +/// let query = q!(Person, Age(12), RiskCategory::High); /// let count = context.count_entities(query); /// ``` #[macro_export] -macro_rules! all { +macro_rules! q { // No properties - generates empty tuple query ($entity:ty) => { $crate::EntityPropertyTuple::<$entity, _>::new(()) @@ -48,9 +48,9 @@ mod tests { fn all_macro_with_add_entity() { let mut context = Context::new(); - // Use all! macro to add an entity + // Use q! macro to add an entity let person = context - .add_entity(all!(TestPerson, Age(42), Risk::High)) + .add_entity(q!(TestPerson, Age(42), Risk::High)) .unwrap(); // Verify properties were set correctly @@ -65,17 +65,17 @@ mod tests { // Add some entities let p1 = context - .add_entity(all!(TestPerson, Age(30), Risk::High)) + .add_entity(q!(TestPerson, Age(30), Risk::High)) .unwrap(); let _ = context - .add_entity(all!(TestPerson, Age(30), Risk::Low)) + .add_entity(q!(TestPerson, Age(30), Risk::Low)) .unwrap(); let _ = context - .add_entity(all!(TestPerson, Age(25), Risk::High)) + .add_entity(q!(TestPerson, Age(25), Risk::High)) .unwrap(); // Sample from entities matching the query - let sampled = context.sample_entity(AllMacroTestRng, all!(TestPerson, Age(30), Risk::High)); + let sampled = context.sample_entity(AllMacroTestRng, q!(TestPerson, Age(30), Risk::High)); assert_eq!(sampled, Some(p1)); } @@ -86,15 +86,15 @@ mod tests { // Add some entities that don't match the query let _ = context - .add_entity(all!(TestPerson, Age(30), Risk::Low)) + .add_entity(q!(TestPerson, Age(30), Risk::Low)) .unwrap(); // Sample should return None when no entities match - let sampled = context.sample_entity(AllMacroTestRng, all!(TestPerson, Age(30), Risk::High)); + let sampled = context.sample_entity(AllMacroTestRng, q!(TestPerson, Age(30), Risk::High)); assert_eq!(sampled, None); } - // Demonstrate that `all!` can disambiguate entities in otherwise ambiguous cases. + // Demonstrate that `q!` can disambiguate entities in otherwise ambiguous cases. use crate::entity::EntityId; define_entity!(TestMammal); define_entity!(TestAvian); @@ -105,18 +105,14 @@ mod tests { fn all_macro_disambiguates_entities() { let mut context = Context::new(); - context - .add_entity(all!(TestAvian, IsBipedal(true))) - .unwrap(); - context - .add_entity(all!(TestMammal, IsBipedal(true))) - .unwrap(); + context.add_entity(q!(TestAvian, IsBipedal(true))).unwrap(); + context.add_entity(q!(TestMammal, IsBipedal(true))).unwrap(); - let result = context.query_result_iterator(all!(TestMammal, IsBipedal(true))); + let result = context.query_result_iterator(q!(TestMammal, IsBipedal(true))); assert_eq!(result.count(), 1); let sampled_id = context - .sample_entity(AllMacroTestRng, all!(TestAvian, IsBipedal(true))) + .sample_entity(AllMacroTestRng, q!(TestAvian, IsBipedal(true))) .unwrap(); let expected_id: EntityId = EntityId::new(0); assert_eq!(sampled_id, expected_id); From 28c576f86e3e370d28be4a5aed3089e32dc45690 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 20:43:17 -0500 Subject: [PATCH 2/8] chore: Update docs and births-deaths example --- docs/book/models/disease_model/src/people.rs | 2 +- .../disease_model/src/transmission_manager.rs | 4 +-- docs/book/src/first_model/people.md | 36 +++++++++---------- docs/book/src/first_model/transmission.md | 13 +++---- docs/book/src/migration_guide.md | 28 ++++----------- docs/book/src/topics/indexing.md | 3 +- .../births-deaths/src/infection_manager.rs | 4 +-- .../births-deaths/src/population_manager.rs | 18 +++++----- .../births-deaths/src/transmission_manager.rs | 9 +++-- src/prelude.rs | 2 +- 10 files changed, 51 insertions(+), 68 deletions(-) diff --git a/docs/book/models/disease_model/src/people.rs b/docs/book/models/disease_model/src/people.rs index ef8f0df1..e13c57c8 100644 --- a/docs/book/models/disease_model/src/people.rs +++ b/docs/book/models/disease_model/src/people.rs @@ -24,7 +24,7 @@ define_property!( pub fn init(context: &mut Context) { trace!("Initializing people"); for _ in 0..POPULATION { - let _: PersonId = context.add_entity(()).expect("failed to add person"); + let _ = context.add_entity(q!(Person)).expect("failed to add person"); } } // ANCHOR_END: init diff --git a/docs/book/models/disease_model/src/transmission_manager.rs b/docs/book/models/disease_model/src/transmission_manager.rs index 78b01dcc..0a6e166b 100644 --- a/docs/book/models/disease_model/src/transmission_manager.rs +++ b/docs/book/models/disease_model/src/transmission_manager.rs @@ -3,7 +3,7 @@ use ixa::prelude::*; use ixa::trace; use rand_distr::Exp; -use crate::people::{InfectionStatus, PersonId}; +use crate::people::{InfectionStatus, Person, PersonId}; use crate::{FORCE_OF_INFECTION, POPULATION}; define_rng!(TransmissionRng); @@ -12,7 +12,7 @@ define_rng!(TransmissionRng); // ANCHOR: attempt_infection fn attempt_infection(context: &mut Context) { trace!("Attempting infection"); - let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap(); + let person_to_infect: PersonId = context.sample_entity(TransmissionRng, q!(Person)).unwrap(); let person_status: InfectionStatus = context.get_property(person_to_infect); if person_status == InfectionStatus::S { diff --git a/docs/book/src/first_model/people.md b/docs/book/src/first_model/people.md index 26949f6a..41887c1d 100644 --- a/docs/book/src/first_model/people.md +++ b/docs/book/src/first_model/people.md @@ -57,29 +57,27 @@ pub fn init(context: &mut Context) { trace!("Initializing people"); for _ in 0..100 { - let _: PersonId = context.add_entity(()).expect("failed to add person"); + let _ = context.add_entity(q!(Person)).expect("failed to add person"); } } ``` -The `context.add_entity()` method call might look a little odd, because we are -not giving `context` any data to insert, but that is because our one and only -`Property` was defined to have a default value of `InfectionStatus::S` -(susceptible)—so `context.add_entity()` doesn't need any information to create a -new person. Another odd thing is the `.expect("failed to add person")` method -call. In more complicated scenarios adding a person can fail. We could intercept -that failure if we wanted, but in this simple case we will just let the program -crash with a message about the reason: "failed to add person". - -Finally, the `Context::add_entity` method returns an entity ID wrapped in a -`Result`, which the `expect` method unwraps. We can use this ID if we need to -refer to this newly created person. Since we don't need it, we assign the value -to the special "don't care" variable `_` (underscore), which just throws the -value away. Why assign it to anything, though? So that the compiler can infer -that it is a `Person` we are creating, as opposed to some other entity we may -have defined. If we just omitted the `let _: PersonId =` part completely, we -would need to explicitly specify the entity type using -[turbo fish notation](../appendix_rust/turbo-fish.md). +We use the `q!` macro to create a query that specifies which entity type to +create. Here, `q!(Person)` with no additional property values means we want a +new `Person` with all default property values—our one and only `Property` was +defined to have a default value of `InfectionStatus::S` (susceptible), so no +additional information is needed. + +The `.expect("failed to add person")` method call handles the case where adding +a person could fail. We could intercept that failure if we wanted, but in this +simple case we will just let the program crash with a message about the reason: +"failed to add person". + +The `Context::add_entity` method returns an entity ID wrapped in a `Result`, +which the `expect` method unwraps. We can use this ID if we need to refer to +this newly created person. Since we don't need it, we assign the value to the +special "don't care" variable `_` (underscore), which just throws the value +away. ## Constants diff --git a/docs/book/src/first_model/transmission.md b/docs/book/src/first_model/transmission.md index 0f3e25d0..8e3b9ec5 100644 --- a/docs/book/src/first_model/transmission.md +++ b/docs/book/src/first_model/transmission.md @@ -88,12 +88,13 @@ The function `attempt_infection()` needs to do the following: Read through this implementation and make sure you understand how it accomplishes the three tasks above. A few observations: -- The method call `context.sample_entity(TransmissionRng, ())` takes the name of - a random number source and a query and returns an `Option\`, which - can have the value of `Some(PersonId)` or `None`. In this case, we give it the - "empty query" `()`, which means we want to sample from the entire population. - The population will never be empty, so the result will never be `None`, and so - we just call `unwrap()` on the `Some(PersonId)` value to get the `PersonId`. +- The method call `context.sample_entity(TransmissionRng, q!(Person))` takes the + name of a random number source and a query and returns an `Option\`, + which can have the value of `Some(PersonId)` or `None`. In this case, we use + the `q!` macro with just the entity type `Person` and no property filters, + which means we want to sample from the entire population. The population will + never be empty, so the result will never be `None`, and so we just call + `unwrap()` on the `Some(PersonId)` value to get the `PersonId`. - If the sampled person is not susceptible, then the only thing this function does is schedule the next attempt at infection. - The time at which the next attempt is scheduled is sampled randomly from the diff --git a/docs/book/src/migration_guide.md b/docs/book/src/migration_guide.md index d581af8b..34938242 100644 --- a/docs/book/src/migration_guide.md +++ b/docs/book/src/migration_guide.md @@ -126,48 +126,32 @@ These macros automatically create a type alias of the form ### Adding a new entity (e.g. a new person) -Adding a new entity with multiple property values: +Adding a new entity with multiple property values using the `q!` macro: ```rust -// Assuming the `Person` entity is defined somewhere. -// Add a new entity (a person in this case) to an existing `Context` instance we have access to. -let person_id = context.add_entity((Age(25), InfectionStatus::Infected)).unwrap(); +let person_id = context.add_entity(q!(Person, Age(25), InfectionStatus::Infected)).unwrap(); ``` Observe: -- The compiler is smart enough to know that we are adding a new `Person` entity - because we supplied a list of property values that are properties for a - `Person`. -- The `add_entity` function takes a "property list", which is just a tuple of +- The `q!` macro takes the entity type as its first argument, followed by any property values. The properties must be distinct, of course, and there must be a value for every "required" property, that is, for every (non-derived) property that doesn't have a default value. -- A single-property tuple uses the syntax `(Age(25), )`. Notice the awkward - trailing comma, which lets the compiler know the parentheses are defining a - tuple rather than functioning as grouping an expression. Adding a new entity with just one property value: ```rust -// Assuming the `Person` entity is defined somewhere. -// Add a new entity (a person in this case) to an existing `Context` instance we have access to. -let person_id = context.add_entity((Age(25), )).unwrap(); +let person_id = context.add_entity(q!(Person, Age(25))).unwrap(); ``` Adding a new entity with only default property values: ```rust -// If you specify the `EntityId\` return type, the compiler uses it to infer which entity to add. -// This is a good practice and avoids the special "turbo fish" syntax. -let person_id: PersonId = context.add_entity(()).unwrap(); - -// If we don't specify the `EntityId\` type, we have to explicitly tell the compiler *which* entity -// type we are adding, as there is nothing from which to infer the entity type. -let person_id = context.add_entity::(()).unwrap(); +let person_id = context.add_entity(q!(Person)).unwrap(); ``` -(These two examples assume there are no required properties, that is, that every +(This example assumes there are no required properties, that is, that every property has a default value.) ### Getting a property value for an entity diff --git a/docs/book/src/topics/indexing.md b/docs/book/src/topics/indexing.md index 9fb82578..e47c65bb 100644 --- a/docs/book/src/topics/indexing.md +++ b/docs/book/src/topics/indexing.md @@ -183,7 +183,8 @@ Suppose we have the properties `AgeGroup` and `InfectionStatus`, and we want to speed up queries of these two properties: ```rust -let age_and_status = context.query_result_iterator((AgeGroup(30), InfectionStatus::Susceptible)); // Bottleneck +let query = q!(Person, AgeGroup(30), InfectionStatus::Susceptible); +let age_and_status = context.query_result_iterator(query); // Bottleneck ``` We could index `AgeGroup` and `InfectionStatus` individually, but in this case diff --git a/examples/births-deaths/src/infection_manager.rs b/examples/births-deaths/src/infection_manager.rs index 8019823b..07135c6f 100644 --- a/examples/births-deaths/src/infection_manager.rs +++ b/examples/births-deaths/src/infection_manager.rs @@ -141,7 +141,7 @@ mod test { let population_size: usize = 10; for index in 0..population_size { - let person = context.add_entity((Age(0),)).unwrap(); + let person = context.add_entity(q!(Person, Age(0))).unwrap(); context.add_plan(1.0, move |context| { context.set_property(person, InfectionStatus::I); @@ -168,7 +168,7 @@ mod test { context.init_random(42); init(&mut context); - let person = context.add_entity((Age(0),)).unwrap(); + let person = context.add_entity(q!(Person, Age(0))).unwrap(); context.add_plan(1.1, move |context| { cancel_recovery_plans(context, person); }); diff --git a/examples/births-deaths/src/population_manager.rs b/examples/births-deaths/src/population_manager.rs index 34ffe163..fe174afa 100644 --- a/examples/births-deaths/src/population_manager.rs +++ b/examples/births-deaths/src/population_manager.rs @@ -72,7 +72,7 @@ fn schedule_birth(context: &mut Context) { .get_global_property_value(Parameters) .unwrap() .clone(); - let person = context.add_entity((Age(0),)).unwrap(); + let person = context.add_entity(q!(Person, Age(0))).unwrap(); context.add_plan(context.get_current_time() + 365.0, move |context| { schedule_aging(context, person); }); @@ -90,7 +90,7 @@ fn schedule_death(context: &mut Context) { .unwrap() .clone(); - if let Some(person) = context.sample_entity(PeopleRng, (Alive(true),)) { + if let Some(person) = context.sample_entity(PeopleRng, q!(Person, Alive(true))) { context.set_property(person, Alive(false)); let next_death_event = context.get_current_time() @@ -110,7 +110,7 @@ pub fn init(context: &mut Context) { for _ in 0..parameters.population { let age: u8 = context.sample_range(PeopleRng, 0..MAX_AGE); - let person = context.add_entity((Age(age),)).unwrap(); + let person = context.add_entity(q!(Person, Age(age))).unwrap(); let birthday = context.sample_distr(PeopleRng, Uniform::new(0.0, 365.0).unwrap()); context.add_plan(365.0 + birthday, move |context| { schedule_aging(context, person); @@ -146,22 +146,22 @@ mod test { fn test_birth_death() { let mut context = Context::new(); - let person1 = context.add_entity((Age(10),)).unwrap(); + let person1 = context.add_entity(q!(Person, Age(10))).unwrap(); let person2 = Rc::>>::new(RefCell::new(None)); let person2_clone = Rc::clone(&person2); context.add_plan(380.0, move |context| { - *person2_clone.borrow_mut() = Some(context.add_entity((Age(0),)).unwrap()); + *person2_clone.borrow_mut() = Some(context.add_entity(q!(Person, Age(0))).unwrap()); }); context.add_plan(400.0, move |context| { context.set_property(person1, Alive(false)); }); context.add_plan(390.0, |context| { - let pop = context.query_entity_count((Alive(true),)); + let pop = context.query_entity_count(q!(Person, Alive(true))); assert_eq!(pop, 2); }); context.add_plan(401.0, |context| { - let pop = context.query_entity_count((Alive(true),)); + let pop = context.query_entity_count(q!(Person, Alive(true))); assert_eq!(pop, 1); }); context.execute(); @@ -221,7 +221,7 @@ mod test { .set_global_property_value(Parameters, p_values.clone()) .unwrap(); context.init_random(p_values.seed); - let _person = context.add_entity((Age(0),)).unwrap(); + let _person = context.add_entity(q!(Person, Age(0))).unwrap(); schedule_death(&mut context); } @@ -238,7 +238,7 @@ mod test { ]; let mut people = Vec::::new(); for age in &age_vec { - people.push(context.add_entity((Age(*age),)).unwrap()); + people.push(context.add_entity(q!(Person, Age(*age))).unwrap()); } for i in 0..people.len() { diff --git a/examples/births-deaths/src/transmission_manager.rs b/examples/births-deaths/src/transmission_manager.rs index d1359fdb..fbe6b656 100644 --- a/examples/births-deaths/src/transmission_manager.rs +++ b/examples/births-deaths/src/transmission_manager.rs @@ -2,14 +2,15 @@ use ixa::prelude::*; use rand_distr::Exp; use crate::parameters_loader::Foi; -use crate::population_manager::{AgeGroupRisk, Alive, InfectionStatus}; +use crate::population_manager::{AgeGroupRisk, Alive, InfectionStatus, Person}; use crate::Parameters; define_rng!(TransmissionRng1); //Attempt infection for specific age group risk (meaning different forces of infection) fn attempt_infection(context: &mut Context, age_group: AgeGroupRisk) { - let population_size: usize = context.query_entity_count((Alive(true), age_group)); + let query = q!(Person, Alive(true), age_group); + let population_size: usize = context.query_entity_count(query); let parameters = context .get_global_property_value(Parameters) .unwrap() @@ -20,9 +21,7 @@ fn attempt_infection(context: &mut Context, age_group: AgeGroupRisk) { .get(&age_group) .unwrap(); if population_size > 0 { - let person_to_infect = context - .sample_entity(TransmissionRng1, (Alive(true), age_group)) - .unwrap(); + let person_to_infect = context.sample_entity(TransmissionRng1, query).unwrap(); let person_status: InfectionStatus = context.get_property(person_to_infect); diff --git a/src/prelude.rs b/src/prelude.rs index 6e11b6cc..6f1def67 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,5 +11,5 @@ pub use crate::report::ContextReportExt; pub use crate::{ define_data_plugin, define_derived_property, define_edge_type, define_entity, define_global_property, define_multi_property, define_property, define_report, define_rng, - impl_edge_type, impl_entity, impl_property, PluginContext, + impl_edge_type, impl_entity, impl_property, q, PluginContext, }; From e5eaf48ddc8e7466eaf2e0d78cfa152ff3c1b70f Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 20:53:22 -0500 Subject: [PATCH 3/8] feat: Implement PropertyList for entities --- docs/book/models/disease_model/Cargo.toml | 3 +- docs/book/src/appendix_rust/turbo-fish.md | 44 ++++++++--------------- docs/book/src/migration_guide.md | 29 ++++++++------- src/entity/context_extension.rs | 8 +++++ src/entity/property_list.rs | 14 ++++++++ 5 files changed, 52 insertions(+), 46 deletions(-) diff --git a/docs/book/models/disease_model/Cargo.toml b/docs/book/models/disease_model/Cargo.toml index 44967585..52056d96 100644 --- a/docs/book/models/disease_model/Cargo.toml +++ b/docs/book/models/disease_model/Cargo.toml @@ -8,6 +8,7 @@ publish = false # Do not publish to the Crates.io registry [dependencies] csv = "1.3.1" -ixa = { git = "https://github.com/CDCgov/ixa", branch = "main" } +# TODO: revert to git dependency before merging +ixa = { path = "../../../.." } rand_distr = "0.5.1" serde = { version = "1.0.217", features = ["derive"] } diff --git a/docs/book/src/appendix_rust/turbo-fish.md b/docs/book/src/appendix_rust/turbo-fish.md index 2a12e661..59024bd1 100644 --- a/docs/book/src/appendix_rust/turbo-fish.md +++ b/docs/book/src/appendix_rust/turbo-fish.md @@ -39,51 +39,35 @@ value, we don't need to supply a value for it when calling create based on the property values we supply, and if we don't supply _any_ property values, we need another way to specify the entity to create. +The simplest way is to pass the entity type directly: + +```rust +context.add_entity(Person).expect("failed to add person"); +``` + The `Context::add_entity` function is actually a whole family of functions `Context::add_entity\`, one function for each concrete `Entity` type `E` and `PropertyList` type `PL`. When we call `context.add_entity(...)` in our code with a tuple of properties (the initialization list), the Rust compiler looks at the initialization list and -uses it to infer the concrete types `E` and `PL`. But when the initialization +uses it to infer the concrete types `E` and `PL`. When the initialization list is `PL=()` (the empty list), the compiler doesn't know what the `Entity` -type `E` should be. The Rust language allows us to tell it explicitly using the -"turbo fish" notation: - -```rust -context.add_entity::(()).expect("failed to add person"); -``` - -Actually, the compiler _always_ already knows the `PropertyList` type `PL`, so -we can use the "wildcard" `_` (underscore) to tell the compiler to infer that -type itself: +type `E` should be. You can avoid this problem entirely by passing the entity +type directly as shown above. Alternatively, you can use turbo fish notation or +specify the return type: ```rust +// Turbo fish notation context.add_entity::(()).expect("failed to add person"); -``` - -There is another way to give the compiler enough information to infer the -`Entity` type, namely by specifying the return type we are expecting. In our -case, we just throw the returned `PersonId` away, but suppose we want to refer -to the newly created person. We could write: -```rust +// Specifying the return type let person_id: PersonId = context.add_entity(()).expect("failed to add person"); ``` -The `Entity` type `E` must be `Person`, because that is the only way -`Context::add_entity\` can return a `PersonId` (a -type alias for `EntityId\`). You can use this trick even if you never -use the returned `PersonId`, but in such a case it's best practice to signal -this intent by using the special "don't care" variable `_` (underscore): - -```rust -let _: PersonId = context.add_entity(()).expect("failed to add person"); -``` - You do not have to learn the rules for when specifying the types using turbo fish notation is required. The compiler will let you know. For `add_entity`, -always specifying the returned type means you'll never have to worry about turbo -fish notation. +passing the entity type directly or specifying the returned type means you'll +never have to worry about turbo fish notation. ## Preferred Idiom for `Context::sample_entity()` diff --git a/docs/book/src/migration_guide.md b/docs/book/src/migration_guide.md index 34938242..56e8ad24 100644 --- a/docs/book/src/migration_guide.md +++ b/docs/book/src/migration_guide.md @@ -126,34 +126,33 @@ These macros automatically create a type alias of the form ### Adding a new entity (e.g. a new person) -Adding a new entity with multiple property values using the `q!` macro: +Adding a new entity with only default property values by passing the entity type +directly: ```rust -let person_id = context.add_entity(q!(Person, Age(25), InfectionStatus::Infected)).unwrap(); +let person_id = context.add_entity(Person).unwrap(); ``` -Observe: +(This example assumes there are no required properties, that is, that every +property has a default value.) -- The `q!` macro takes the entity type as its first argument, followed by any - property values. The properties must be distinct, of course, and there must be - a value for every "required" property, that is, for every (non-derived) - property that doesn't have a default value. - -Adding a new entity with just one property value: +Adding a new entity with property values using the `q!` macro: ```rust -let person_id = context.add_entity(q!(Person, Age(25))).unwrap(); +let person_id = context.add_entity(q!(Person, Age(25), InfectionStatus::Infected)).unwrap(); ``` -Adding a new entity with only default property values: +The `q!` macro takes the entity type as its first argument, followed by any +property values. The properties must be distinct, of course, and there must be +a value for every "required" property, that is, for every (non-derived) +property that doesn't have a default value. + +Adding a new entity with just one property value: ```rust -let person_id = context.add_entity(q!(Person)).unwrap(); +let person_id = context.add_entity(q!(Person, Age(25))).unwrap(); ``` -(This example assumes there are no required properties, that is, that every -property has a default value.) - ### Getting a property value for an entity ```rust diff --git a/src/entity/context_extension.rs b/src/entity/context_extension.rs index c4b32120..161985fd 100644 --- a/src/entity/context_extension.rs +++ b/src/entity/context_extension.rs @@ -485,6 +485,14 @@ mod tests { assert_eq!(context.get_entity_count::(), 3); } + #[test] + fn add_entity_with_zst() { + let mut context = Context::new(); + let animal = context.add_entity(Animal).unwrap(); + assert_eq!(context.get_entity_count::(), 1); + assert_eq!(context.get_property::(animal), Legs(4)); + } + // Helper for index tests #[derive(Copy, Clone)] enum IndexMode { diff --git a/src/entity/property_list.rs b/src/entity/property_list.rs index 045c72aa..a86f34f8 100644 --- a/src/entity/property_list.rs +++ b/src/entity/property_list.rs @@ -58,6 +58,20 @@ impl PropertyList for () { } } +// An Entity ZST itself is an empty `PropertyList` for that entity. +// This allows `context.add_entity(Person)` instead of `context.add_entity(())`. +impl PropertyList for E { + fn validate() -> Result<(), IxaError> { + Ok(()) + } + fn contains_properties(property_type_ids: &[TypeId]) -> bool { + property_type_ids.is_empty() + } + fn set_values_for_entity(&self, _entity_id: EntityId, _property_store: &PropertyStore) { + // No values to assign. + } +} + // ToDo(RobertJacobsonCDC): The following is a fundamental limitation in Rust. If downstream code *can* implement a // trait impl that will cause conflicting implementations with some blanket impl, it disallows it, regardless of // whether the conflict actually exists. From 1c2c060ed58d31b349d58cd516070a2ec41c59d7 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 20:57:29 -0500 Subject: [PATCH 4/8] feat: impl query for the ZST and fix the other examples --- examples/basic-infection/src/people.rs | 2 +- .../src/transmission_manager.rs | 4 +-- examples/network-hhmodel/incidence_report.rs | 2 +- examples/network-hhmodel/main.rs | 2 +- src/entity/query/query_impls.rs | 34 +++++++++++++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/examples/basic-infection/src/people.rs b/examples/basic-infection/src/people.rs index 61488301..5374bd14 100644 --- a/examples/basic-infection/src/people.rs +++ b/examples/basic-infection/src/people.rs @@ -20,6 +20,6 @@ define_property!( pub fn init(context: &mut Context) { trace!("Initializing people"); for _ in 0..POPULATION { - let _: PersonId = context.add_entity(()).unwrap(); + let _ = context.add_entity(Person).unwrap(); } } diff --git a/examples/basic-infection/src/transmission_manager.rs b/examples/basic-infection/src/transmission_manager.rs index e98338ea..4a0081a2 100644 --- a/examples/basic-infection/src/transmission_manager.rs +++ b/examples/basic-infection/src/transmission_manager.rs @@ -10,7 +10,7 @@ define_rng!(TransmissionRng); fn attempt_infection(context: &mut Context) { trace!("Attempting infection"); let population_size: usize = context.get_entity_count::(); - let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap(); + let person_to_infect: PersonId = context.sample_entity(TransmissionRng, Person).unwrap(); let person_status: InfectionStatus = context.get_property(person_to_infect); @@ -52,7 +52,7 @@ mod test { fn test_attempt_infection() { let mut context = Context::new(); context.init_random(SEED); - let person_id: PersonId = context.add_entity(()).unwrap(); + let person_id = context.add_entity(Person).unwrap(); attempt_infection(&mut context); let person_status: InfectionStatus = context.get_property(person_id); assert_eq!(person_status, InfectionStatus::I); diff --git a/examples/network-hhmodel/incidence_report.rs b/examples/network-hhmodel/incidence_report.rs index a20d4a58..874d9232 100644 --- a/examples/network-hhmodel/incidence_report.rs +++ b/examples/network-hhmodel/incidence_report.rs @@ -155,7 +155,7 @@ mod test { }, ); - let to_infect: Vec = vec![context.sample_entity(MainRng, ()).unwrap()]; + let to_infect: Vec = vec![context.sample_entity(MainRng, Person).unwrap()]; #[allow(clippy::vec_init_then_push)] seir::init(&mut context, &to_infect); diff --git a/examples/network-hhmodel/main.rs b/examples/network-hhmodel/main.rs index a4e566d4..fee887dc 100644 --- a/examples/network-hhmodel/main.rs +++ b/examples/network-hhmodel/main.rs @@ -40,7 +40,7 @@ fn initialize(context: &mut Context) { incidence_report::init(context).unwrap(); // Initialize infected person with InfectedBy value equal to their own PersonId - let to_infect: Vec = vec![context.sample_entity(MainRng, ()).unwrap()]; + let to_infect: Vec = vec![context.sample_entity(MainRng, Person).unwrap()]; #[allow(clippy::vec_init_then_push)] seir::init(context, &to_infect); diff --git a/src/entity/query/query_impls.rs b/src/entity/query/query_impls.rs index 0ad5fd62..0d8a7787 100644 --- a/src/entity/query/query_impls.rs +++ b/src/entity/query/query_impls.rs @@ -43,6 +43,40 @@ impl Query for () { } } +// An Entity ZST itself is an empty query matching all entities of that type. +// This allows `context.sample_entity(Rng, Person)` instead of `context.sample_entity(Rng, ())`. +impl Query for E { + fn get_query(&self) -> Vec<(usize, HashValueType)> { + Vec::new() + } + + fn get_type_ids(&self) -> Vec { + Vec::new() + } + + fn multi_property_id(&self) -> Option { + None + } + + fn multi_property_value_hash(&self) -> HashValueType { + let empty: &[u128] = &[]; + one_shot_128(&empty) + } + + 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) + } + + fn match_entity(&self, _entity_id: EntityId, _context: &Context) -> bool { + true + } + + fn filter_entities(&self, _entities: &mut Vec>, _context: &Context) { + // Nothing to do. + } +} + // Implement the query version with one parameter. impl> Query for (P1,) { fn get_query(&self) -> Vec<(usize, HashValueType)> { From 0d18aff94f6e2af6989c115e1ca996dffcf5d02e Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 20:59:38 -0500 Subject: [PATCH 5/8] fix: use result in the book example --- docs/book/models/disease_model/src/people.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/book/models/disease_model/src/people.rs b/docs/book/models/disease_model/src/people.rs index e13c57c8..330e8104 100644 --- a/docs/book/models/disease_model/src/people.rs +++ b/docs/book/models/disease_model/src/people.rs @@ -21,10 +21,11 @@ define_property!( // ANCHOR: init /// Populates the "world" with the `POPULATION` number of people. -pub fn init(context: &mut Context) { +pub fn init(context: &mut Context) -> Result<(), IxaError> { trace!("Initializing people"); for _ in 0..POPULATION { - let _ = context.add_entity(q!(Person)).expect("failed to add person"); + let _ = context.add_entity(Person)?; } + Ok(()) } // ANCHOR_END: init From 32bc4f95af875acf2574f18c8bf964aaf6972585 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 21:09:46 -0500 Subject: [PATCH 6/8] docs: update docs --- docs/book/models/disease_model/src/people.rs | 5 ++--- .../models/disease_model/src/transmission_manager.rs | 4 ++-- docs/book/src/first_model/people.md | 10 ++++------ docs/book/src/first_model/transmission.md | 7 ++++--- examples/basic-infection/src/transmission_manager.rs | 4 ++-- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/book/models/disease_model/src/people.rs b/docs/book/models/disease_model/src/people.rs index 330e8104..b159906f 100644 --- a/docs/book/models/disease_model/src/people.rs +++ b/docs/book/models/disease_model/src/people.rs @@ -21,11 +21,10 @@ define_property!( // ANCHOR: init /// Populates the "world" with the `POPULATION` number of people. -pub fn init(context: &mut Context) -> Result<(), IxaError> { +pub fn init(context: &mut Context) { trace!("Initializing people"); for _ in 0..POPULATION { - let _ = context.add_entity(Person)?; + let _ = context.add_entity(Person).expect("failed to add person"); } - Ok(()) } // ANCHOR_END: init diff --git a/docs/book/models/disease_model/src/transmission_manager.rs b/docs/book/models/disease_model/src/transmission_manager.rs index 0a6e166b..b6646e60 100644 --- a/docs/book/models/disease_model/src/transmission_manager.rs +++ b/docs/book/models/disease_model/src/transmission_manager.rs @@ -3,7 +3,7 @@ use ixa::prelude::*; use ixa::trace; use rand_distr::Exp; -use crate::people::{InfectionStatus, Person, PersonId}; +use crate::people::{InfectionStatus, Person}; use crate::{FORCE_OF_INFECTION, POPULATION}; define_rng!(TransmissionRng); @@ -12,7 +12,7 @@ define_rng!(TransmissionRng); // ANCHOR: attempt_infection fn attempt_infection(context: &mut Context) { trace!("Attempting infection"); - let person_to_infect: PersonId = context.sample_entity(TransmissionRng, q!(Person)).unwrap(); + let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap(); let person_status: InfectionStatus = context.get_property(person_to_infect); if person_status == InfectionStatus::S { diff --git a/docs/book/src/first_model/people.md b/docs/book/src/first_model/people.md index 41887c1d..e38baa6d 100644 --- a/docs/book/src/first_model/people.md +++ b/docs/book/src/first_model/people.md @@ -57,16 +57,14 @@ pub fn init(context: &mut Context) { trace!("Initializing people"); for _ in 0..100 { - let _ = context.add_entity(q!(Person)).expect("failed to add person"); + let _ = context.add_entity(Person).expect("failed to add person"); } } ``` -We use the `q!` macro to create a query that specifies which entity type to -create. Here, `q!(Person)` with no additional property values means we want a -new `Person` with all default property values—our one and only `Property` was -defined to have a default value of `InfectionStatus::S` (susceptible), so no -additional information is needed. +We use `Person` here to represent a new entity with all default property values– +our one and only `Property` was defined to have a default value of +`InfectionStatus::S` (susceptible), so no additional information is needed. The `.expect("failed to add person")` method call handles the case where adding a person could fail. We could intercept that failure if we wanted, but in this diff --git a/docs/book/src/first_model/transmission.md b/docs/book/src/first_model/transmission.md index 8e3b9ec5..b47bea20 100644 --- a/docs/book/src/first_model/transmission.md +++ b/docs/book/src/first_model/transmission.md @@ -88,11 +88,12 @@ The function `attempt_infection()` needs to do the following: Read through this implementation and make sure you understand how it accomplishes the three tasks above. A few observations: -- The method call `context.sample_entity(TransmissionRng, q!(Person))` takes the +- The method call `context.sample_entity(TransmissionRng, Person)` takes the name of a random number source and a query and returns an `Option\`, which can have the value of `Some(PersonId)` or `None`. In this case, we use - the `q!` macro with just the entity type `Person` and no property filters, - which means we want to sample from the entire population. The population will + `Person` and no property filters, which means we want to sample from the + entire population. If we wanted to, we could pass filters with the `q!` macro + (e.g., `q!(Person, Region("California"))`) The population will never be empty, so the result will never be `None`, and so we just call `unwrap()` on the `Some(PersonId)` value to get the `PersonId`. - If the sampled person is not susceptible, then the only thing this function diff --git a/examples/basic-infection/src/transmission_manager.rs b/examples/basic-infection/src/transmission_manager.rs index 4a0081a2..bbbb7409 100644 --- a/examples/basic-infection/src/transmission_manager.rs +++ b/examples/basic-infection/src/transmission_manager.rs @@ -2,7 +2,7 @@ use ixa::prelude::*; use ixa::trace; use rand_distr::Exp; -use crate::people::{InfectionStatus, Person, PersonId}; +use crate::people::{InfectionStatus, Person}; use crate::{FOI, MAX_TIME}; define_rng!(TransmissionRng); @@ -10,7 +10,7 @@ define_rng!(TransmissionRng); fn attempt_infection(context: &mut Context) { trace!("Attempting infection"); let population_size: usize = context.get_entity_count::(); - let person_to_infect: PersonId = context.sample_entity(TransmissionRng, Person).unwrap(); + let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap(); let person_status: InfectionStatus = context.get_property(person_to_infect); From 9184a895263d13f7ad9e6d38fe19cf1dcce97ec8 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 21:16:44 -0500 Subject: [PATCH 7/8] docs: Remove turbofish example --- docs/book/src/appendix_rust/turbo-fish.md | 118 +--------------------- 1 file changed, 4 insertions(+), 114 deletions(-) diff --git a/docs/book/src/appendix_rust/turbo-fish.md b/docs/book/src/appendix_rust/turbo-fish.md index 59024bd1..bf48b834 100644 --- a/docs/book/src/appendix_rust/turbo-fish.md +++ b/docs/book/src/appendix_rust/turbo-fish.md @@ -6,116 +6,7 @@ once. A nice feature of Rust is that the compiler can often infer the generic types at the point the function is used instead of relying on the programmer to specify the types explicitly. -## An example with `Context::add_entity()` - -Suppose we want to initialize a population: - -```rust -define_entity!(Person); -define_property!( - // The type of the property - enum InfectionStatus {S,I,R}, - // The entity the property is associated with - Person, - // The property's default value for newly created `Person` entities - default_const = InfectionStatus::S -); - -/// Populates the "world" with people. -pub fn init(context: &mut Context) { - for _ in 0..1000 { - context.add_entity((InfectionStatus::S, )).expect("failed to add person"); - } -} -``` - -During the initialization of our population, we explicitly told ixa to create a -new _susceptible_ person, that is, with the `InfectionStatus::S` property value. -However, when we defined the `InfectionStatus` property with the -`define_property!` macro, we specified a default initial value with -`default_const = InfectionStatus::S`. Since `InfectionStatus` has a default -value, we don't need to supply a value for it when calling -`context.add_entity(...)`. But remember, the compiler infers _which_ entity to -create based on the property values we supply, and if we don't supply _any_ -property values, we need another way to specify the entity to create. - -The simplest way is to pass the entity type directly: - -```rust -context.add_entity(Person).expect("failed to add person"); -``` - -The `Context::add_entity` function is actually a whole family of functions -`Context::add_entity\`, one function for each -concrete `Entity` type `E` and `PropertyList` type `PL`. When we call -`context.add_entity(...)` in our code with a tuple of properties (the -initialization list), the Rust compiler looks at the initialization list and -uses it to infer the concrete types `E` and `PL`. When the initialization -list is `PL=()` (the empty list), the compiler doesn't know what the `Entity` -type `E` should be. You can avoid this problem entirely by passing the entity -type directly as shown above. Alternatively, you can use turbo fish notation or -specify the return type: - -```rust -// Turbo fish notation -context.add_entity::(()).expect("failed to add person"); - -// Specifying the return type -let person_id: PersonId = context.add_entity(()).expect("failed to add person"); -``` - -You do not have to learn the rules for when specifying the types using turbo -fish notation is required. The compiler will let you know. For `add_entity`, -passing the entity type directly or specifying the returned type means you'll -never have to worry about turbo fish notation. - -## Preferred Idiom for `Context::sample_entity()` - -The `Context::sample_entity()` method especially deserves discussion, because we -often want to immediately use the returned value. If we try to use the standard -Rust idiom to express this, we have to specify the types using turbo fish, which -is awkward and ugly: - -```rust -// Sample from the entire population by supplying the "empty" query. The last two `_`s are for the query type and -// RNG type, both of which the compiler can infer. -if let Some(person_id) = context.sample_entity::(TransmissionRng, ()) { - // Do something with `person_id`... -} -``` - -Since we are sampling from the entire population, if `sample_entity` returns -`None`, then the population is empty, and we clearly have a bug in our code, in -which case the best thing to do is to crash the program and fix the bug. Thus, -instead of the `if let Some(...) =` construct, it's actually better to just call -`unwrap` on the returned value in this case. Here is a much more readable and -simple way to write the code: - -```rust -// Sample from the entire population by supplying the "empty" query. The compiler infers which entity to sample -// from the type of the variable we assign to. -let person_id: PersonId = context.sample_entity(()).unwrap(); -// Do something with `person_id`... -``` - -If you really want to check for the `None` case in your code, assign the return -value to a variable of type `Option\` instead of immediately -unwrapping the `PersonId` value. Then you can use `if let Some(...) =` or a -`match` statement at your preference: - -```rust -let maybe_person_id: Option = context.sample_entity(()); -match maybe_person_id { - Some(person_id) => { - // Do something with `person_id` - } - None => { - // Handle the empty population case - } -} -``` - -## Other Examples +## Examples The compiler's ability to infer the types of generic functions means that for most of the common functions the types do not need to be specified with turbo @@ -130,14 +21,13 @@ let status: InfectionStatus = context.get_property(person_id); context.set_property(other_person_id, status); ``` -As with `Context::add_entity`, the generic types for querying and sampling -methods can usually be inferred by the compiler except when the "empty" query is -provided: +The generic types for querying and sampling +methods can usually be inferred by the compiler: ```rust // A silly example, but no turbo fish is required. context.with_query_results( - (Age(30), Alive(true)), + q!(Person, Age(30), Alive(true)), |people_set| println("{:?}", people_set) ); ``` From 1db2349c43a4dd209d21b0dbf89eda63af5f5ea5 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 20 Feb 2026 21:27:30 -0500 Subject: [PATCH 8/8] fix: actually use with --- docs/book/src/appendix_rust/turbo-fish.md | 2 +- docs/book/src/first_model/transmission.md | 4 +- docs/book/src/migration_guide.md | 8 ++-- docs/book/src/topics/indexing.md | 2 +- .../births-deaths/src/infection_manager.rs | 4 +- .../births-deaths/src/population_manager.rs | 18 ++++----- .../births-deaths/src/transmission_manager.rs | 2 +- src/entity/query/mod.rs | 34 ++++++++-------- src/macros/mod.rs | 2 +- src/macros/{q.rs => with.rs} | 40 +++++++++++-------- src/prelude.rs | 2 +- 11 files changed, 62 insertions(+), 56 deletions(-) rename src/macros/{q.rs => with.rs} (69%) diff --git a/docs/book/src/appendix_rust/turbo-fish.md b/docs/book/src/appendix_rust/turbo-fish.md index bf48b834..bb42c784 100644 --- a/docs/book/src/appendix_rust/turbo-fish.md +++ b/docs/book/src/appendix_rust/turbo-fish.md @@ -27,7 +27,7 @@ methods can usually be inferred by the compiler: ```rust // A silly example, but no turbo fish is required. context.with_query_results( - q!(Person, Age(30), Alive(true)), + with!(Person, Age(30), Alive(true)), |people_set| println("{:?}", people_set) ); ``` diff --git a/docs/book/src/first_model/transmission.md b/docs/book/src/first_model/transmission.md index b47bea20..c2fcdca5 100644 --- a/docs/book/src/first_model/transmission.md +++ b/docs/book/src/first_model/transmission.md @@ -92,8 +92,8 @@ accomplishes the three tasks above. A few observations: name of a random number source and a query and returns an `Option\`, which can have the value of `Some(PersonId)` or `None`. In this case, we use `Person` and no property filters, which means we want to sample from the - entire population. If we wanted to, we could pass filters with the `q!` macro - (e.g., `q!(Person, Region("California"))`) The population will + entire population. If we wanted to, we could pass filters with the `with!` macro + (e.g., `with!(Person, Region("California"))`) The population will never be empty, so the result will never be `None`, and so we just call `unwrap()` on the `Some(PersonId)` value to get the `PersonId`. - If the sampled person is not susceptible, then the only thing this function diff --git a/docs/book/src/migration_guide.md b/docs/book/src/migration_guide.md index 56e8ad24..7d5b7aa9 100644 --- a/docs/book/src/migration_guide.md +++ b/docs/book/src/migration_guide.md @@ -136,13 +136,13 @@ let person_id = context.add_entity(Person).unwrap(); (This example assumes there are no required properties, that is, that every property has a default value.) -Adding a new entity with property values using the `q!` macro: +Adding a new entity with property values using the `with!` macro: ```rust -let person_id = context.add_entity(q!(Person, Age(25), InfectionStatus::Infected)).unwrap(); +let person_id = context.add_entity(with!(Person, Age(25), InfectionStatus::Infected)).unwrap(); ``` -The `q!` macro takes the entity type as its first argument, followed by any +The `with!` macro takes the entity type as its first argument, followed by any property values. The properties must be distinct, of course, and there must be a value for every "required" property, that is, for every (non-derived) property that doesn't have a default value. @@ -150,7 +150,7 @@ property that doesn't have a default value. Adding a new entity with just one property value: ```rust -let person_id = context.add_entity(q!(Person, Age(25))).unwrap(); +let person_id = context.add_entity(with!(Person, Age(25))).unwrap(); ``` ### Getting a property value for an entity diff --git a/docs/book/src/topics/indexing.md b/docs/book/src/topics/indexing.md index e47c65bb..78acfffd 100644 --- a/docs/book/src/topics/indexing.md +++ b/docs/book/src/topics/indexing.md @@ -183,7 +183,7 @@ Suppose we have the properties `AgeGroup` and `InfectionStatus`, and we want to speed up queries of these two properties: ```rust -let query = q!(Person, AgeGroup(30), InfectionStatus::Susceptible); +let query = with!(Person, AgeGroup(30), InfectionStatus::Susceptible); let age_and_status = context.query_result_iterator(query); // Bottleneck ``` diff --git a/examples/births-deaths/src/infection_manager.rs b/examples/births-deaths/src/infection_manager.rs index 07135c6f..82fc4c38 100644 --- a/examples/births-deaths/src/infection_manager.rs +++ b/examples/births-deaths/src/infection_manager.rs @@ -141,7 +141,7 @@ mod test { let population_size: usize = 10; for index in 0..population_size { - let person = context.add_entity(q!(Person, Age(0))).unwrap(); + let person = context.add_entity(with!(Person, Age(0))).unwrap(); context.add_plan(1.0, move |context| { context.set_property(person, InfectionStatus::I); @@ -168,7 +168,7 @@ mod test { context.init_random(42); init(&mut context); - let person = context.add_entity(q!(Person, Age(0))).unwrap(); + let person = context.add_entity(with!(Person, Age(0))).unwrap(); context.add_plan(1.1, move |context| { cancel_recovery_plans(context, person); }); diff --git a/examples/births-deaths/src/population_manager.rs b/examples/births-deaths/src/population_manager.rs index fe174afa..1b2351c4 100644 --- a/examples/births-deaths/src/population_manager.rs +++ b/examples/births-deaths/src/population_manager.rs @@ -72,7 +72,7 @@ fn schedule_birth(context: &mut Context) { .get_global_property_value(Parameters) .unwrap() .clone(); - let person = context.add_entity(q!(Person, Age(0))).unwrap(); + let person = context.add_entity(with!(Person, Age(0))).unwrap(); context.add_plan(context.get_current_time() + 365.0, move |context| { schedule_aging(context, person); }); @@ -90,7 +90,7 @@ fn schedule_death(context: &mut Context) { .unwrap() .clone(); - if let Some(person) = context.sample_entity(PeopleRng, q!(Person, Alive(true))) { + if let Some(person) = context.sample_entity(PeopleRng, with!(Person, Alive(true))) { context.set_property(person, Alive(false)); let next_death_event = context.get_current_time() @@ -110,7 +110,7 @@ pub fn init(context: &mut Context) { for _ in 0..parameters.population { let age: u8 = context.sample_range(PeopleRng, 0..MAX_AGE); - let person = context.add_entity(q!(Person, Age(age))).unwrap(); + let person = context.add_entity(with!(Person, Age(age))).unwrap(); let birthday = context.sample_distr(PeopleRng, Uniform::new(0.0, 365.0).unwrap()); context.add_plan(365.0 + birthday, move |context| { schedule_aging(context, person); @@ -146,22 +146,22 @@ mod test { fn test_birth_death() { let mut context = Context::new(); - let person1 = context.add_entity(q!(Person, Age(10))).unwrap(); + let person1 = context.add_entity(with!(Person, Age(10))).unwrap(); let person2 = Rc::>>::new(RefCell::new(None)); let person2_clone = Rc::clone(&person2); context.add_plan(380.0, move |context| { - *person2_clone.borrow_mut() = Some(context.add_entity(q!(Person, Age(0))).unwrap()); + *person2_clone.borrow_mut() = Some(context.add_entity(with!(Person, Age(0))).unwrap()); }); context.add_plan(400.0, move |context| { context.set_property(person1, Alive(false)); }); context.add_plan(390.0, |context| { - let pop = context.query_entity_count(q!(Person, Alive(true))); + let pop = context.query_entity_count(with!(Person, Alive(true))); assert_eq!(pop, 2); }); context.add_plan(401.0, |context| { - let pop = context.query_entity_count(q!(Person, Alive(true))); + let pop = context.query_entity_count(with!(Person, Alive(true))); assert_eq!(pop, 1); }); context.execute(); @@ -221,7 +221,7 @@ mod test { .set_global_property_value(Parameters, p_values.clone()) .unwrap(); context.init_random(p_values.seed); - let _person = context.add_entity(q!(Person, Age(0))).unwrap(); + let _person = context.add_entity(with!(Person, Age(0))).unwrap(); schedule_death(&mut context); } @@ -238,7 +238,7 @@ mod test { ]; let mut people = Vec::::new(); for age in &age_vec { - people.push(context.add_entity(q!(Person, Age(*age))).unwrap()); + people.push(context.add_entity(with!(Person, Age(*age))).unwrap()); } for i in 0..people.len() { diff --git a/examples/births-deaths/src/transmission_manager.rs b/examples/births-deaths/src/transmission_manager.rs index fbe6b656..9d6794fd 100644 --- a/examples/births-deaths/src/transmission_manager.rs +++ b/examples/births-deaths/src/transmission_manager.rs @@ -9,7 +9,7 @@ define_rng!(TransmissionRng1); //Attempt infection for specific age group risk (meaning different forces of infection) fn attempt_infection(context: &mut Context, age_group: AgeGroupRisk) { - let query = q!(Person, Alive(true), age_group); + let query = with!(Person, Alive(true), age_group); let population_size: usize = context.query_entity_count(query); let parameters = context .get_global_property_value(Parameters) diff --git a/src/entity/query/mod.rs b/src/entity/query/mod.rs index 44c9b07f..c61e2e3d 100644 --- a/src/entity/query/mod.rs +++ b/src/entity/query/mod.rs @@ -15,7 +15,7 @@ use crate::{Context, IxaError}; /// A newtype wrapper that associates a tuple of property values with an entity type. /// -/// This is not meant to be used directly, but rather as a backing for the q! macro/ +/// This is not meant to be used directly, but rather as a backing for the with! macro/ /// a replacement for the query tuple. /// /// # Example @@ -26,7 +26,7 @@ use crate::{Context, IxaError}; /// define_property!(struct Age(u8), Person, default_const = Age(0)); /// /// // Use the all macro -/// let query = q!(Person, Age(42)); +/// let query = with!(Person, Age(42)); /// // Under the hood this is: /// // EntityPropertyTuple::::new((Age(42),)); /// ``` @@ -627,42 +627,42 @@ mod tests { #[test] fn all_macro_no_properties() { - use crate::q; + use crate::with; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::Low)).unwrap(); - // q!(Person) should match all Person entities - let query = q!(Person); + // with!(Person) should match all Person entities + let query = with!(Person); assert_eq!(context.query_entity_count(query), 2); } #[test] fn all_macro_single_property() { - use crate::q; + use crate::with; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap(); - // q!(Person, Age(42)) should match entities with Age = 42 - let query = q!(Person, Age(42)); + // with!(Person, Age(42)) should match entities with Age = 42 + let query = with!(Person, Age(42)); assert_eq!(context.query_entity_count(query), 2); } #[test] fn all_macro_multiple_properties() { - use crate::q; + use crate::with; let mut context = Context::new(); let p1 = context.add_entity((Age(42), RiskCategory::High)).unwrap(); let _ = context.add_entity((Age(42), RiskCategory::Low)).unwrap(); let _ = context.add_entity((Age(30), RiskCategory::High)).unwrap(); - // q!(Person, Age(42), RiskCategory::High) should match one entity - let query = q!(Person, Age(42), RiskCategory::High); + // with!(Person, Age(42), RiskCategory::High) should match one entity + let query = with!(Person, Age(42), RiskCategory::High); assert_eq!(context.query_entity_count(query), 1); context.with_query_results(query, &mut |people| { @@ -672,16 +672,16 @@ mod tests { #[test] fn all_macro_with_trailing_comma() { - use crate::q; + use crate::with; let mut context = Context::new(); let _ = context.add_entity((Age(42), RiskCategory::High)).unwrap(); // Trailing comma should work - let query = q!(Person, Age(42)); + let query = with!(Person, Age(42)); assert_eq!(context.query_entity_count(query), 1); - let query = q!(Person, Age(42), RiskCategory::High); + let query = with!(Person, Age(42), RiskCategory::High); assert_eq!(context.query_entity_count(query), 1); } @@ -713,12 +713,12 @@ mod tests { #[test] fn all_macro_as_property_list_for_add_entity() { - use crate::q; + use crate::with; let mut context = Context::new(); - // Use q! macro result to add an entity - let props = q!(Person, Age(42), RiskCategory::High); + // Use with! macro result to add an entity + let props = with!(Person, Age(42), RiskCategory::High); let person = context.add_entity(props).unwrap(); // Verify the entity was created with the correct properties diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 01d38f6c..f1c3ddc1 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -6,4 +6,4 @@ mod define_rng; mod edge_impl; mod entity_impl; mod property_impl; -mod q; +mod with; diff --git a/src/macros/q.rs b/src/macros/with.rs similarity index 69% rename from src/macros/q.rs rename to src/macros/with.rs index e474be54..06104c6c 100644 --- a/src/macros/q.rs +++ b/src/macros/with.rs @@ -4,18 +4,18 @@ /// /// ```ignore /// // Add an entity with default properties -/// let query = q!(Person); +/// let query = with!(Person); /// context.add_entity(query)?; /// /// // An inline query matching a single property -/// let person = context.sample_entity(MyRng, q!(Person, Age(12)))?; +/// let person = context.sample_entity(MyRng, with!(Person, Age(12)))?; /// /// // A query matching multiple properties -/// let query = q!(Person, Age(12), RiskCategory::High); +/// let query = with!(Person, Age(12), RiskCategory::High); /// let count = context.count_entities(query); /// ``` #[macro_export] -macro_rules! q { +macro_rules! with { // No properties - generates empty tuple query ($entity:ty) => { $crate::EntityPropertyTuple::<$entity, _>::new(()) @@ -48,9 +48,9 @@ mod tests { fn all_macro_with_add_entity() { let mut context = Context::new(); - // Use q! macro to add an entity + // Use with! macro to add an entity let person = context - .add_entity(q!(TestPerson, Age(42), Risk::High)) + .add_entity(with!(TestPerson, Age(42), Risk::High)) .unwrap(); // Verify properties were set correctly @@ -65,17 +65,18 @@ mod tests { // Add some entities let p1 = context - .add_entity(q!(TestPerson, Age(30), Risk::High)) + .add_entity(with!(TestPerson, Age(30), Risk::High)) .unwrap(); let _ = context - .add_entity(q!(TestPerson, Age(30), Risk::Low)) + .add_entity(with!(TestPerson, Age(30), Risk::Low)) .unwrap(); let _ = context - .add_entity(q!(TestPerson, Age(25), Risk::High)) + .add_entity(with!(TestPerson, Age(25), Risk::High)) .unwrap(); // Sample from entities matching the query - let sampled = context.sample_entity(AllMacroTestRng, q!(TestPerson, Age(30), Risk::High)); + let sampled = + context.sample_entity(AllMacroTestRng, with!(TestPerson, Age(30), Risk::High)); assert_eq!(sampled, Some(p1)); } @@ -86,15 +87,16 @@ mod tests { // Add some entities that don't match the query let _ = context - .add_entity(q!(TestPerson, Age(30), Risk::Low)) + .add_entity(with!(TestPerson, Age(30), Risk::Low)) .unwrap(); // Sample should return None when no entities match - let sampled = context.sample_entity(AllMacroTestRng, q!(TestPerson, Age(30), Risk::High)); + let sampled = + context.sample_entity(AllMacroTestRng, with!(TestPerson, Age(30), Risk::High)); assert_eq!(sampled, None); } - // Demonstrate that `q!` can disambiguate entities in otherwise ambiguous cases. + // Demonstrate that `with!` can disambiguate entities in otherwise ambiguous cases. use crate::entity::EntityId; define_entity!(TestMammal); define_entity!(TestAvian); @@ -105,14 +107,18 @@ mod tests { fn all_macro_disambiguates_entities() { let mut context = Context::new(); - context.add_entity(q!(TestAvian, IsBipedal(true))).unwrap(); - context.add_entity(q!(TestMammal, IsBipedal(true))).unwrap(); + context + .add_entity(with!(TestAvian, IsBipedal(true))) + .unwrap(); + context + .add_entity(with!(TestMammal, IsBipedal(true))) + .unwrap(); - let result = context.query_result_iterator(q!(TestMammal, IsBipedal(true))); + let result = context.query_result_iterator(with!(TestMammal, IsBipedal(true))); assert_eq!(result.count(), 1); let sampled_id = context - .sample_entity(AllMacroTestRng, q!(TestAvian, IsBipedal(true))) + .sample_entity(AllMacroTestRng, with!(TestAvian, IsBipedal(true))) .unwrap(); let expected_id: EntityId = EntityId::new(0); assert_eq!(sampled_id, expected_id); diff --git a/src/prelude.rs b/src/prelude.rs index 6f1def67..7e202a85 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,5 +11,5 @@ pub use crate::report::ContextReportExt; pub use crate::{ define_data_plugin, define_derived_property, define_edge_type, define_entity, define_global_property, define_multi_property, define_property, define_report, define_rng, - impl_edge_type, impl_entity, impl_property, q, PluginContext, + impl_edge_type, impl_entity, impl_property, with, PluginContext, };