Skip to main content

ixa/macros/
property_impl.rs

1/*!
2
3Macros for implementing properties.
4
5# [`define_property!`][macro@crate::define_property]
6
7For the most common cases, use the [`define_property!`][macro@crate::define_property] macro. This macro defines a struct or enum
8with the standard derives required by the [`Property`][crate::entity::property::Property] trait and implements [`Property`][crate::entity::property::Property] (via
9[`impl_property!`][macro@crate::impl_property]) for you.
10
11```rust,ignore
12define_property!(struct Age(u8), Person);
13define_property!(struct Location(City, State), Person);
14define_property!(
15    enum InfectionStatus {
16        Susceptible,
17        Infectious,
18        Recovered,
19    },
20    Person,
21    default_const = InfectionStatus::Susceptible
22);
23```
24
25Notice the convenient `default_const = <default_value>` keyword argument that allows you to
26define a compile-time constant default value for the property. This is an optional argument.
27If it is omitted, a value for the property must be supplied upon entity creation.
28
29The primary advantage of using this macro is that it automatically derives the list of traits every
30[`Property`][crate::entity::property::Property] needs to derive for you. You don't have to remember them. You also get a cute syntax for
31specifying the default value, but it's not much harder to specify default values using other macros.
32
33If you need the macro to generate `Eq` and/or `Hash` manually instead of deriving them, use the
34optional `impl_eq_hash = ...` argument with one of the following values: `Eq`, `Hash`, `both`, or
35`neither`.
36
37Notice you need to use the `struct` or `enum` keywords, but you don't need to
38specify the visibility. A `pub` visibility is added automatically to the struct
39and to inner fields of tuple structs in the expansion.
40
41# [`impl_property!`][macro@crate::impl_property]
42
43You can implement [`Property`][crate::entity::property::Property] for existing types using the
44[`impl_property!`][macro@crate::impl_property] macro. This macro defines the
45[`Property`][crate::entity::property::Property] trait implementation for you but doesn't take care
46of the `#[derive(..)]` boilerplate, so you have to remember to derive or implement the traits
47required by [`Property`][crate::entity::property::Property] for your type: `Copy`, `Clone`,
48`Debug`, and `PartialEq`. If you want to index the property, it must also implement `Eq` and
49`Hash`.
50
51If the type cannot derive `PartialEq` / `Eq` or `Hash`, for example because it contains `f32` or
52`f64`, use [`impl_property_eq!`][macro@crate::impl_property_eq],
53[`impl_property_hash!`][macro@crate::impl_property_hash], or
54[`impl_property_eq_hash!`][macro@crate::impl_property_eq_hash] to generate those implementations
55for the manually declared type. These macros require `ixa::rkyv::Archive` and `ixa::rkyv::Serialize` derives
56because they compare and hash the type's archived byte representation.
57
58Some examples:
59
60```rust,ignore
61define_entity!(Person);
62
63// The `define_property!` automatically adds `pub` visibility to the struct and its tuple fields. If
64// we want to restrict the visibility of our `Property` type, we can use the `impl_property!` macro
65// instead. The only catch is, we have to remember to derive or implement the traits required by
66// `Property`. We also derive `Eq` and `Hash` here so the property can be indexed.
67#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
68struct Age(pub u8);
69impl_property!(Age, Person);
70
71// Here we derive `Default`, which also requires an attribute on one
72// of the variants. (`Property` has its own independent mechanism for
73// assigning default values for entities unrelated to the `Default` trait.)
74#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
75enum InfectionStatus {
76    #[default]
77    Susceptible,
78    Infected,
79    Recovered,
80}
81// We also specify the default value explicitly for entities.
82impl_property!(InfectionStatus, Person, default_const = InfectionStatus::Susceptible);
83
84// Exactly equivalent to
85//    `define_property!(struct Vaccinated(pub bool), Person, default_const = Vaccinated(false));`
86#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
87pub struct Vaccinated(pub bool);
88impl_property!(Vaccinated, Person, default_const = Vaccinated(false));
89
90// For manually declared floating-point properties, generate byte-based equality and hashing.
91#[derive(
92    Copy,
93    Clone,
94    Debug,
95    serde::Serialize,
96    serde::Deserialize,
97    ixa::rkyv::Archive,
98    ixa::rkyv::Serialize,
99)]
100#[rkyv(crate = ixa::rkyv)]
101struct Weight(pub f64);
102impl_property_eq_hash!(Weight);
103impl_property!(Weight, Person, default_const = Weight(0.0));
104```
105
106# [`impl_property!`][macro@crate::impl_property] with options
107
108The [`impl_property!`][macro@crate::impl_property] macro gives you much more control over the implementation of your
109property type. It takes optional keyword arguments for things like the default value,
110initialization strategy, and how the property is converted to a string for display.
111
112Non-derived properties either have a default constant value for new entities
113(`default_const = ...`), or a value is required to be provided for new entities
114(no `default_const`).
115
116```rust,ignore
117impl_property!(
118    InfectionStatus,
119    Person,
120    default_const = InfectionStatus::Susceptible,
121    display_impl = |v| format!("status: {v:?}")
122);
123```
124
125*/
126
127/// Defines a `struct` or `enum` with a standard set of derives and automatically invokes
128/// [`impl_property!`][macro@crate::impl_property] for it. This macro provides a concise shorthand for defining
129/// simple property types that follow the same derive and implementation pattern.
130///
131/// The macro supports the following forms:
132///
133/// ### 1. Tuple Structs
134/// ```rust
135/// # use ixa::{define_entity, define_property};
136/// # define_entity!(Person);
137/// define_property!(struct Age(u8), Person);
138/// ```
139/// Expands to:
140/// ```rust
141/// # use ixa::{impl_property, define_entity};
142/// # define_entity!(Person);
143/// #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
144/// pub struct Age(u8);
145/// impl_property!(Age, Person);
146/// ```
147///
148/// You can define multiple tuple fields:
149/// ```rust,ignore
150/// define_property!(struct Location(City, State), Person);
151/// ```
152///
153/// ### 2. Named-field Structs
154/// ```rust
155/// # use ixa::{define_property, define_entity};
156/// # define_entity!(Person);
157/// define_property!(struct Coordinates { x: i32, y: i32 }, Person);
158/// ```
159/// Expands to:
160/// ```rust
161/// # use ixa::{impl_property, define_entity};
162/// # define_entity!(Person);
163/// #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
164/// pub struct Coordinates { x: i32, y: i32 }
165/// impl_property!(Coordinates, Person);
166/// ```
167///
168/// ### 3. Enums
169/// ```rust
170/// # use ixa::{define_property, define_entity};
171/// # define_entity!(Person);
172/// define_property!(
173///     enum InfectionStatus {
174///         Susceptible,
175///         Infectious,
176///         Recovered,
177///     },
178///     Person
179/// );
180/// ```
181/// Expands to:
182/// ```rust
183/// # use ixa::{impl_property, define_entity};
184/// # define_entity!(Person);
185/// #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
186/// pub enum InfectionStatus {
187///     Susceptible,
188///     Infectious,
189///     Recovered,
190/// }
191/// impl_property!(InfectionStatus, Person);
192/// ```
193///
194/// ### Notes
195///
196/// - By default, the generated type derives `Debug`, `PartialEq`, `Eq`, `Hash`, `Clone`, and `Copy`.
197/// - Use the optional `default_const = <default_value>` argument to define a compile-time constant
198///   default for the property.
199/// - Use `impl_eq_hash = Eq`, `Hash`, `both`, or `neither` as the first optional argument to suppress the default
200///   `Eq`/`Hash` derives and switch to generated or user-supplied implementations.
201/// - Remaining optional arguments follow the same ordering as [`impl_property!`][macro@crate::impl_property].
202/// - If you need a more complex type definition (e.g., generics, attributes, or non-`Copy`
203///   fields), define the type manually and then call [`impl_property!`][macro@crate::impl_property] directly.
204#[macro_export]
205macro_rules! define_property {
206    // Implementation Notes
207    //
208    // To implement the optional `impl_eq_hash` keyword argument, we have the following choices:
209    //
210    // 1. Have a single public match branch per type form with `$(, impl_eq_hash =
211    //    $impl_eq_hash:ident)?`, but explicitly list all the keyword options. This option disallows
212    //    the `$(, $($extra:tt)+)*` pattern for the tail.
213    // 2. Have two branches per type form, one with the `impl_eq_hash = ...` keyword present and one
214    //    with it absent, and use the `$(, $($extra:tt)+)*` pattern for the tail. This duplicates the
215    //    number of public match arms, but it allows us to keep the keyword arguments defined in
216    //    `impl_property!` instead of repeated throughout the code.
217    // 3. Use a proc macro or "TT munching", both of which are more heavy weight.
218    //
219    // We choose the second option. Unfortunately, this doesn't completely eliminate repetition of
220    // the list of keyword arguments. We still have them in the
221    // `impl_derived_property!(@with_option_display_default ...)` and
222    // `impl_property!(@with_option_display_default ...)` subcommands.
223
224    (
225        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
226        $entity:ident,
227        impl_eq_hash = $impl_eq_hash:ident
228        $(, $($extra:tt)*)?
229    ) => {
230        $crate::define_property!(
231            @apply_property_decoration $impl_eq_hash,
232            pub struct $name(pub Option<$inner_ty>);,
233            $name
234        );
235        $crate::impl_property!(@with_option_display_default $name, $entity $(, $($extra)*)?);
236    };
237    (
238        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
239        $entity:ident
240        $(, $($extra:tt)*)?
241    ) => {
242        $crate::define_property!(
243            @apply_property_decoration ,
244            pub struct $name(pub Option<$inner_ty>);,
245            $name
246        );
247        $crate::impl_property!(@with_option_display_default $name, $entity $(, $($extra)*)?);
248    };
249
250    (
251        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
252        $entity:ident,
253        impl_eq_hash = $impl_eq_hash:ident
254        $(, $($extra:tt)*)?
255    ) => {
256        $crate::define_property!(
257            @apply_property_decoration $impl_eq_hash,
258            pub struct $name($(pub $field_ty),*);,
259            $name
260        );
261        $crate::impl_property!($name, $entity $(, $($extra)*)?);
262    };
263    (
264        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
265        $entity:ident
266        $(, $($extra:tt)*)?
267    ) => {
268        $crate::define_property!(
269            @apply_property_decoration ,
270            pub struct $name($(pub $field_ty),*);,
271            $name
272        );
273        $crate::impl_property!($name, $entity $(, $($extra)*)?);
274    };
275
276    (
277        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
278        $entity:ident,
279        impl_eq_hash = $impl_eq_hash:ident
280        $(, $($extra:tt)*)?
281    ) => {
282        $crate::define_property!(
283            @apply_property_decoration $impl_eq_hash,
284            pub struct $name { $(pub $field_name : $field_ty),* },
285            $name
286        );
287        $crate::impl_property!($name, $entity $(, $($extra)*)?);
288    };
289    (
290        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
291        $entity:ident
292        $(, $($extra:tt)*)?
293    ) => {
294        $crate::define_property!(
295            @apply_property_decoration ,
296            pub struct $name { $(pub $field_name : $field_ty),* },
297            $name
298        );
299        $crate::impl_property!($name, $entity $(, $($extra)*)?);
300    };
301
302    (
303        enum $name:ident {
304            $($variant:ident),* $(,)?
305        },
306        $entity:ident,
307        impl_eq_hash = $impl_eq_hash:ident
308        $(, $($extra:tt)*)?
309    ) => {
310        $crate::define_property!(
311            @apply_property_decoration $impl_eq_hash,
312            pub enum $name { $($variant),* },
313            $name
314        );
315        $crate::impl_property!($name, $entity $(, $($extra)*)?);
316    };
317    (
318        enum $name:ident {
319            $($variant:ident),* $(,)?
320        },
321        $entity:ident
322        $(, $($extra:tt)*)?
323    ) => {
324        $crate::define_property!(
325            @apply_property_decoration ,
326            pub enum $name { $($variant),* },
327            $name
328        );
329        $crate::impl_property!($name, $entity $(, $($extra)*)?);
330    };
331
332    // Both `define_property!` and `define_derived_property!` need to attach derives to a
333    // concrete item, so the mode table lives here as a shared internal subcommand.
334    (@apply_property_decoration , $item:item, $name:ident) => {
335        #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
336        $item
337    };
338    (@apply_property_decoration Eq, $item:item, $name:ident) => {
339        #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, $crate::rkyv::Archive, $crate::rkyv::Serialize)]
340        #[rkyv(crate = $crate::rkyv)]
341        $item
342        $crate::impl_property_eq!($name);
343    };
344    (@apply_property_decoration Hash, $item:item, $name:ident) => {
345        #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, $crate::rkyv::Archive, $crate::rkyv::Serialize)]
346        #[rkyv(crate = $crate::rkyv)]
347        $item
348        $crate::impl_property_hash!($name);
349    };
350    (@apply_property_decoration both, $item:item, $name:ident) => {
351        #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, $crate::rkyv::Archive, $crate::rkyv::Serialize)]
352        #[rkyv(crate = $crate::rkyv)]
353        $item
354        $crate::impl_property_eq_hash!($name);
355    };
356    (@apply_property_decoration neither, $item:item, $name:ident) => {
357        #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
358        $item
359    };
360    (@apply_property_decoration $mode:ident, $item:item, $name:ident) => {
361        compile_error!("`impl_eq_hash` must be one of `Eq`, `Hash`, `both`, or `neither`");
362    };
363
364}
365
366/// Implements [`PartialEq`][core::cmp::PartialEq] and [`Eq`][core::cmp::Eq] for a property type
367/// using Ixa's generated byte-based equality behavior.
368///
369/// This macro is useful when declaring a property type manually and then using
370/// [`impl_property!`][macro@crate::impl_property] or
371/// [`impl_derived_property!`][macro@crate::impl_derived_property]. The type must implement
372/// [`rkyv::Archive`][crate::rkyv::Archive] and [`rkyv::Serialize`][crate::rkyv::Serialize], because
373/// equality is computed by serializing the archived representation of each value.
374///
375/// The macro accepts a concrete property type identifier. Generic property types are not supported.
376#[macro_export]
377macro_rules! impl_property_eq {
378    ($name:ident) => {
379        impl core::cmp::PartialEq for $name {
380            fn eq(&self, other: &Self) -> bool {
381                const N: usize = core::mem::size_of::<<$name as $crate::rkyv::Archive>::Archived>();
382
383                let left = $crate::rkyv::api::high::to_bytes_in::<_, $crate::rkyv::rancor::Error>(
384                    self,
385                    $crate::hashing::EqualityBufferWriter::<N>::new(),
386                )
387                .expect("serializing left value for equality comparison failed");
388
389                let right = $crate::rkyv::api::high::to_bytes_in::<_, $crate::rkyv::rancor::Error>(
390                    other,
391                    $crate::hashing::EqualityBufferWriter::<N>::new(),
392                )
393                .expect("serializing right value for equality comparison failed");
394
395                left.as_written() == right.as_written()
396            }
397        }
398
399        impl core::cmp::Eq for $name {}
400    };
401}
402
403/// Implements [`Hash`][core::hash::Hash] for a property type using Ixa's generated byte-based
404/// hashing behavior.
405///
406/// This macro is useful when declaring a property type manually and then using
407/// [`impl_property!`][macro@crate::impl_property] or
408/// [`impl_derived_property!`][macro@crate::impl_derived_property]. The type must implement
409/// [`rkyv::Archive`][crate::rkyv::Archive] and [`rkyv::Serialize`][crate::rkyv::Serialize], because
410/// hashing is computed by serializing the archived representation into the supplied hasher.
411///
412/// The macro accepts a concrete property type identifier. Generic property types are not supported.
413#[macro_export]
414macro_rules! impl_property_hash {
415    ($name:ident) => {
416        impl core::hash::Hash for $name {
417            fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
418                $crate::rkyv::api::high::to_bytes_in::<_, $crate::rkyv::rancor::Error>(
419                    self,
420                    $crate::hashing::HasherWriter::new(state),
421                )
422                .expect("serialization failed while hashing");
423            }
424        }
425    };
426}
427
428/// Implements [`PartialEq`][core::cmp::PartialEq], [`Eq`][core::cmp::Eq], and
429/// [`Hash`][core::hash::Hash] for a property type using Ixa's generated byte-based equality and
430/// hashing behavior.
431///
432/// This is a convenience wrapper around [`impl_property_eq!`][macro@crate::impl_property_eq] and
433/// [`impl_property_hash!`][macro@crate::impl_property_hash].
434///
435/// The macro accepts a concrete property type identifier. Generic property types are not supported.
436#[macro_export]
437macro_rules! impl_property_eq_hash {
438    ($name:ident) => {
439        $crate::impl_property_eq!($name);
440        $crate::impl_property_hash!($name);
441    };
442}
443
444/// Implements the [`Property`][crate::entity::property::Property] trait for the given property type and entity.
445///
446/// Use this macro when you want to implement the `Property<E: Entity>` trait for a type you have declared yourself.
447/// You might want to declare your own property type yourself instead of using the [`define_property!`][macro@crate::define_property] macro if
448/// - you want a visibility other than `pub`
449/// - you want to derive additional traits
450/// - your type definition requires attribute proc-macros or other special syntax (for example, deriving
451///   `Default` on an enum requires an attribute on one of the variants)
452///
453/// Example:
454///
455/// In this example, in addition to the set of derives required for all property types, we also derive the `Default`
456/// trait for an enum type, which requires the proc-macro attribute `#[default]` on one of the variants.
457///
458/// ```rust
459/// # use ixa::{impl_property, define_entity};
460/// # define_entity!(Person);
461/// #[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
462/// pub enum InfectionStatus {
463///     #[default]
464///     Susceptible,
465///     Infectious,
466///     Recovered,
467/// }
468/// // We also specify that this property is assigned a default value for new entities if a value isn't provided.
469/// // Here we have it coincide with `Default::default()`, but this isn't required.
470/// impl_property!(InfectionStatus, Person, default_const = InfectionStatus::Susceptible);
471/// ```
472///
473/// # Parameters
474///
475/// Parameters must be given in the correct order.
476///
477/// * `$property`: The identifier for the type implementing [`Property`][crate::entity::property::Property].
478/// * `$entity`: The entity type this property is associated with.
479/// * Optional parameters (each may be omitted; defaults will be used):
480///   * `compute_derived_fn = <expr>` — Function used to compute derived properties. Use `define_derived_property!` or
481///     `impl_derived_property!` instead of using this option directly.
482///   * `default_const = <expr>` — Constant default value if the property has one; implies a non-derived property.
483///   * `display_impl = <expr>` — Function converting the property value to a string; defaults to `|v| format!("{v:?}")`.
484/// * Optional parameters that should generally be left alone, used internally to implement derived properties and
485///   multi-properties:
486///   * `collect_deps_fn = <expr>` — Function used to collect property dependencies; defaults to an empty implementation.
487///   * `ctor_registration = <expr>` — Code run in the `ctor` for property registration.
488///
489/// # Semantics
490/// - If `compute_derived_fn` is provided, the property is derived. In this case, `default_const` must be absent, and
491///   calling `Property::default_const()` results in a panic. Use `define_derived_property!` or `impl_derived_property!`
492///   instead of using this option directly.
493/// - If `default_const` is provided, the property is a non-derived constant property. In this case,
494///   `compute_derived_fn` must be absent, and calling `Property::compute_derived()` results in a panic.
495/// - If neither is provided, the property is non-derived and required/explicit; both `Property::default_const()` and
496///   `Property::compute_derived()` panic.
497/// - If both are provided, a compile-time error is emitted.
498#[macro_export]
499macro_rules! impl_property {
500    (
501        $property:ident,
502        $entity:ident
503        $(, compute_derived_fn = $compute_derived_fn:expr)?
504        $(, default_const = $default_const:expr)?
505        $(, collect_deps_fn = $collect_deps_fn:expr)?
506        $(, display_impl = $display_impl:expr)?
507        $(, ctor_registration = $ctor_registration:expr)?
508    ) => {
509        // Enforce mutual exclusivity at compile time.
510        $crate::impl_property!(@assert_not_both $($compute_derived_fn)? ; $($default_const)?);
511
512        $crate::impl_property!(
513            @__impl_property_common
514            $property,
515            $entity,
516
517            // initialization_kind (implicit)
518            $crate::impl_property!(@select_initialization_kind $($compute_derived_fn)? ; $($default_const)?),
519
520            // compute_derived_fn (panic unless explicitly provided)
521            $crate::impl_property!(
522                @unwrap_or
523                $($compute_derived_fn)?,
524                |_, _| panic!("property {} is not derived", stringify!($property))
525            ),
526
527            // default_const (panic unless explicitly provided)
528            $crate::impl_property!(
529                @unwrap_or
530                $($default_const)?,
531                panic!("property {} has no default value", stringify!($property))
532            ),
533
534            // query_parts_type
535            [&'a dyn std::any::Any; 1],
536
537            // value_from_query_parts_fn
538            {
539                |parts: &[&dyn std::any::Any]| -> Option<$property> {
540                    let [part] = parts else {
541                        return None;
542                    };
543                    part.downcast_ref::<$property>().copied()
544                }
545            },
546
547            // query_parts_for_value_fn
548            {
549                fn default_query_parts_for_value<'a>(value: &'a $property) -> [&'a dyn std::any::Any; 1] {
550                    [value as &'a dyn std::any::Any]
551                }
552
553                default_query_parts_for_value
554            },
555
556            // type_id_fn
557            {
558                std::any::TypeId::of::<Self>()
559            },
560
561            // display_impl
562            $crate::impl_property!(@unwrap_or $($display_impl)?, |v| format!("{v:?}")),
563
564            // collect_deps_fn
565            $crate::impl_property!(
566                @unwrap_or
567                $($collect_deps_fn)?,
568                |_| {/* Do nothing */}
569            ),
570
571            // ctor_registration
572            $crate::impl_property!(@unwrap_or $($ctor_registration)?, {
573                $crate::entity::property_store::add_to_property_registry::<$entity, $property>();
574            }),
575        );
576    };
577
578    (
579        @with_option_display_default
580        $property:ident,
581        $entity:ident
582        $(, compute_derived_fn = $compute_derived_fn:expr)?
583        $(, default_const = $default_const:expr)?
584        $(, collect_deps_fn = $collect_deps_fn:expr)?
585        $(, display_impl = $display_impl:expr)?
586        $(, ctor_registration = $ctor_registration:expr)?
587    ) => {
588        $crate::impl_property!(
589            $property,
590            $entity
591            $(, compute_derived_fn = $compute_derived_fn)?
592            $(, default_const = $default_const)?
593            $(, collect_deps_fn = $collect_deps_fn)?
594            , display_impl = $crate::impl_property!(@unwrap_or $($display_impl)?, |value: &Self| {
595                match value.0 {
596                    Some(v) => format!("{:?}", v),
597                    None => "None".to_string(),
598                }
599            })
600            $(, ctor_registration = $ctor_registration)?
601        );
602    };
603
604    (
605        @multi_property
606        $property:ident,
607        $entity:ident,
608        ( $($dependency:ident),+ )
609        $(, compute_derived_fn = $compute_derived_fn:expr)?
610        $(, default_const = $default_const:expr)?
611        $(, collect_deps_fn = $collect_deps_fn:expr)?
612        $(, display_impl = $display_impl:expr)?
613        $(, ctor_registration = $ctor_registration:expr)?
614    ) => {
615        $crate::impl_property!(@assert_not_both $($compute_derived_fn)? ; $($default_const)?);
616
617        $crate::impl_property!(
618            @__impl_property_common
619            $property,
620            $entity,
621            $crate::impl_property!(@select_initialization_kind $($compute_derived_fn)? ; $($default_const)?),
622            $crate::impl_property!(
623                @unwrap_or
624                $($compute_derived_fn)?,
625                |_, _| panic!("property {} is not derived", stringify!($property))
626            ),
627            $crate::impl_property!(
628                @unwrap_or
629                $($default_const)?,
630                panic!("property {} has no default value", stringify!($property))
631            ),
632            [&'a dyn std::any::Any; $crate::impl_property!(@count_tts $($dependency),+)],
633            {
634                $crate::paste::paste! {
635                    #[allow(unused_assignments)]
636                    |parts: &[&dyn std::any::Any]| -> Option<$property> {
637                        let [$( [<p_ $dependency:lower>] ),+,] = parts else {
638                            return None;
639                        };
640                        let sorted_parts = [$( [<p_ $dependency:lower>] ),+];
641                        let keys = [
642                            $(
643                                <$dependency as $crate::entity::property::Property<$entity>>::type_id(),
644                            )+
645                        ];
646                        let indices = $crate::entity::multi_property::static_sorted_indices(&keys);
647                        let inverse = $crate::entity::multi_property::static_inverse_indices(&indices);
648                        let mut declared_index = 0usize;
649                        Some((
650                            $(
651                                {
652                                    let value = *sorted_parts[inverse[declared_index]].downcast_ref::<$dependency>()?;
653                                    declared_index += 1;
654                                    value
655                                },
656                            )+
657                        ))
658                    }
659                }
660            },
661            {
662                $crate::paste::paste! {
663                    fn multi_property_query_parts_for_value<'a>(
664                        value: &'a $property,
665                    ) -> [&'a dyn std::any::Any; $crate::impl_property!(@count_tts $($dependency),+)] {
666                        let keys = [
667                            $(
668                                <$dependency as $crate::entity::property::Property<$entity>>::type_id(),
669                            )+
670                        ];
671                        let ( $( [<_ $dependency:lower>] ),+ ) = value;
672                        let mut parts = [
673                            $(
674                                [<_ $dependency:lower>] as &'a dyn std::any::Any,
675                            )+
676                        ];
677                        $crate::entity::multi_property::static_reorder_by_keys(&keys, &mut parts);
678                        parts
679                    }
680
681                    multi_property_query_parts_for_value
682                }
683            },
684            {
685                std::any::TypeId::of::<Self>()
686            },
687            $crate::impl_property!(@unwrap_or $($display_impl)?, |v| format!("{v:?}")),
688            $crate::impl_property!(@unwrap_or $($collect_deps_fn)?, |_| {/* Do nothing */}),
689            $crate::impl_property!(@unwrap_or $($ctor_registration)?, {
690                $crate::entity::property_store::add_to_property_registry::<$entity, $property>();
691            }),
692        );
693    };
694
695    // Compile-time mutual exclusivity check.
696    (@assert_not_both $compute_derived_fn:expr ; $default_const:expr) => {
697        compile_error!(
698            "impl_property!: `compute_derived_fn = ...` (derived property) and `default_const = ...` \
699             (non-derived property default constant) are mutually exclusive. Remove one of them."
700        );
701    };
702    (@assert_not_both $compute_derived_fn:expr ; ) => {};
703    (@assert_not_both ; $default_const:expr) => {};
704    (@assert_not_both ; ) => {};
705
706    // Select initialization kind (implicit).
707    (@select_initialization_kind $compute_derived_fn:expr ; $default_const:expr) => {
708        // This arm should be unreachable because @assert_not_both triggers first, but keep it
709        // as a backstop if the macro is used incorrectly.
710        compile_error!(
711            "impl_property!: cannot select initialization kind because both `compute_derived_fn` \
712             and `default_const` are present"
713        )
714    };
715    (@select_initialization_kind $compute_derived_fn:expr ; ) => {
716        $crate::entity::property::PropertyInitializationKind::Derived
717    };
718    (@select_initialization_kind ; $default_const:expr) => {
719        $crate::entity::property::PropertyInitializationKind::Constant
720    };
721    (@select_initialization_kind ; ) => {
722        $crate::entity::property::PropertyInitializationKind::Explicit
723    };
724
725    // Helpers for defaults, a pair per macro parameter type (`expr`, `ty`).
726    (@unwrap_or $value:expr, $_default:expr) => { $value };
727    (@unwrap_or, $default:expr) => { $default };
728
729    (@replace_with_unit $_tt:tt) => { () };
730    (@count_tts $($tt:tt),* $(,)?) => {
731        <[()]>::len(&[$($crate::impl_property!(@replace_with_unit $tt)),*])
732    };
733
734    // This is the purely syntactic implementation.
735    (
736        @__impl_property_common
737        $property:ident,           // The name of the type we are implementing `Property` for
738        $entity:ident,             // The entity type this property is associated with
739        $initialization_kind:expr, // The kind of initialization this property has (implicit selection)
740        $compute_derived_fn:expr,  // If the property is derived, the function that computes the value
741        $default_const:expr,       // If the property has a constant default initial value, the default value
742        $query_parts_type:ty,
743        $value_from_query_parts_fn:expr,
744        $query_parts_for_value_fn:expr,
745        $type_id_fn:expr,          // Code that returns the logical type ID for this property
746        $display_impl:expr,        // A function that takes a value and returns a string representation of this property
747        $collect_deps_fn:expr,     // If the property is derived, the function that computes the value
748        $ctor_registration:expr,   // Code that runs in a ctor for property registration
749    ) => {
750        impl $crate::entity::property::Property<$entity> for $property {
751            type QueryParts<'a> = $query_parts_type where Self: 'a;
752
753            const NAME: &'static str = stringify!($property);
754
755            fn initialization_kind() -> $crate::entity::property::PropertyInitializationKind {
756                $initialization_kind
757            }
758
759            fn compute_derived(
760                _context: &$crate::Context,
761                _entity_id: $crate::entity::EntityId<$entity>,
762            ) -> Self {
763                ($compute_derived_fn)(_context, _entity_id)
764            }
765
766            fn default_const() -> Self {
767                $default_const
768            }
769
770            fn value_from_query_parts(
771                parts: &[&dyn std::any::Any],
772            ) -> Option<Self> {
773                ($value_from_query_parts_fn)(parts)
774            }
775
776            fn query_parts_for_value(value: &Self) -> Self::QueryParts<'_> {
777                ($query_parts_for_value_fn)(value)
778            }
779
780            fn type_id() -> std::any::TypeId {
781                $type_id_fn
782            }
783
784            fn get_display(&self) -> String {
785                ($display_impl)(self)
786            }
787
788            fn id() -> usize {
789                // This static must be initialized with a compile-time constant expression.
790                // We use `usize::MAX` as a sentinel to mean "uninitialized". This
791                // static variable is shared among all instances of this concrete item type.
792                static INDEX: std::sync::atomic::AtomicUsize =
793                    std::sync::atomic::AtomicUsize::new(usize::MAX);
794
795                // Fast path: already initialized.
796                let index = INDEX.load(std::sync::atomic::Ordering::Relaxed);
797                if index != usize::MAX {
798                    return index;
799                }
800
801                // Slow path: initialize it.
802                $crate::entity::property_store::initialize_property_id::<$entity>(&INDEX)
803            }
804
805            fn query_identity_id() -> usize {
806                static IDENTITY: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
807
808                *IDENTITY.get_or_init(|| {
809                    $crate::entity::multi_property::intern_query_identity_raw::<$entity>(&[
810                        <Self as $crate::entity::property::Property<$entity>>::type_id(),
811                    ])
812                })
813            }
814
815            fn collect_non_derived_dependencies(result: &mut $crate::HashSet<usize>) {
816                ($collect_deps_fn)(result)
817            }
818        }
819
820        $crate::paste::paste! {
821            $crate::ctor::declarative::ctor!{
822                #[ctor(unsafe)]
823                fn [<_register_property_ $entity:snake _ $property:snake>]() {
824                    $ctor_registration
825                }
826            }
827        }
828    };
829}
830
831/// The "derived" variant of [`define_property!`][macro@crate::define_property] for defining simple derived property types.
832/// Defines a `struct` or `enum` with a standard set of derives and automatically invokes
833/// [`impl_derived_property!`][macro@crate::impl_derived_property] for it.
834///
835/// Defines a derived property with the following parameters:
836/// * Property type declaration: A struct or enum declaration.
837/// * `$entity`: The name of the entity of which the new type is a property.
838/// * `[$($dependency),+]`: A list of person properties the derived property depends on.
839/// * `[$(global_dependency),*]`: A list of global properties the derived property depends on. Can optionally be omitted if empty.
840/// * `$calculate`: A closure that takes the values of each dependency and returns the derived value.
841/// * Optional parameters: The same optional parameters accepted by [`impl_property!`][macro@crate::impl_property],
842///   plus `impl_eq_hash = Eq | Hash | both | neither` to control whether `Eq`/`Hash` are derived or generated
843///   for the declared type, mirroring [`define_property!`][macro@crate::define_property].
844#[macro_export]
845macro_rules! define_derived_property {
846    // Implementation Notes
847    //
848    // We reuse `define_property!`'s shared decoration helper, then delegate the derived-property
849    // behavior to `impl_derived_property!`.
850    //
851    // See `derive_property!` implementation notes for why each type form is duplicated.
852
853    // Struct (tuple) with single Option<T> field
854    (
855        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
856        $entity:ident,
857        [$($dependency:ident),*]
858        $(, [$($global_dependency:ident),*])?,
859        |$($param:ident),+| $derive_fn:expr,
860        impl_eq_hash = $impl_eq_hash:ident
861        $(, $($extra:tt)+)*
862    ) => {
863        $crate::define_property!(
864            @apply_property_decoration
865            $impl_eq_hash,
866            pub struct $name(pub Option<$inner_ty>);,
867            $name
868        );
869
870        $crate::impl_derived_property!(
871            @with_option_display_default
872            $name,
873            $entity,
874            [$($dependency),*],
875            [$($($global_dependency),*)?],
876            |$($param),+| $derive_fn
877            $(, $($extra)+)*
878        );
879    };
880    (
881        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
882        $entity:ident,
883        [$($dependency:ident),*]
884        $(, [$($global_dependency:ident),*])?,
885        |$($param:ident),+| $derive_fn:expr
886        $(, $($extra:tt)+)*
887    ) => {
888        $crate::define_property!(
889            @apply_property_decoration
890            ,
891            pub struct $name(pub Option<$inner_ty>);,
892            $name
893        );
894
895        $crate::impl_derived_property!(
896            @with_option_display_default
897            $name,
898            $entity,
899            [$($dependency),*],
900            [$($($global_dependency),*)?],
901            |$($param),+| $derive_fn
902            $(, $($extra)+)*
903        );
904    };
905
906    // Struct (tuple)
907    (
908        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
909        $entity:ident,
910        [$($dependency:ident),*]
911        $(, [$($global_dependency:ident),*])?,
912        |$($param:ident),+| $derive_fn:expr,
913        impl_eq_hash = $impl_eq_hash:ident
914        $(, $($extra:tt)+)*
915    ) => {
916        $crate::define_property!(
917            @apply_property_decoration
918            $impl_eq_hash,
919            pub struct $name( $(pub $field_ty),* );,
920            $name
921        );
922
923        $crate::impl_derived_property!(
924            $name,
925            $entity,
926            [$($dependency),*],
927            [$($($global_dependency),*)?],
928            |$($param),+| $derive_fn
929            $(, $($extra)+)*
930        );
931    };
932    (
933        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
934        $entity:ident,
935        [$($dependency:ident),*]
936        $(, [$($global_dependency:ident),*])?,
937        |$($param:ident),+| $derive_fn:expr
938        $(, $($extra:tt)+)*
939    ) => {
940        $crate::define_property!(
941            @apply_property_decoration
942            ,
943            pub struct $name( $(pub $field_ty),* );,
944            $name
945        );
946
947        $crate::impl_derived_property!(
948            $name,
949            $entity,
950            [$($dependency),*],
951            [$($($global_dependency),*)?],
952            |$($param),+| $derive_fn
953            $(, $($extra)+)*
954        );
955    };
956
957    // Struct (named fields)
958    (
959        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
960        $entity:ident,
961        [$($dependency:ident),*]
962        $(, [$($global_dependency:ident),*])?,
963        |$($param:ident),+| $derive_fn:expr,
964        impl_eq_hash = $impl_eq_hash:ident
965        $(, $($extra:tt)+)*
966    ) => {
967        $crate::define_property!(
968            @apply_property_decoration
969            $impl_eq_hash,
970            pub struct $name { $($visibility $field_name : $field_ty),* },
971            $name
972        );
973
974        $crate::impl_derived_property!(
975            $name,
976            $entity,
977            [$($dependency),*],
978            [$($($global_dependency),*)?],
979            |$($param),+| $derive_fn
980            $(, $($extra)+)*
981        );
982    };
983    (
984        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
985        $entity:ident,
986        [$($dependency:ident),*]
987        $(, [$($global_dependency:ident),*])?,
988        |$($param:ident),+| $derive_fn:expr
989        $(, $($extra:tt)+)*
990    ) => {
991        $crate::define_property!(
992            @apply_property_decoration
993            ,
994            pub struct $name { $($visibility $field_name : $field_ty),* },
995            $name
996        );
997
998        $crate::impl_derived_property!(
999            $name,
1000            $entity,
1001            [$($dependency),*],
1002            [$($($global_dependency),*)?],
1003            |$($param),+| $derive_fn
1004            $(, $($extra)+)*
1005        );
1006    };
1007
1008    // Enum
1009    (
1010        enum $name:ident {
1011            $($variant:ident),* $(,)?
1012        },
1013        $entity:ident,
1014        [$($dependency:ident),*]
1015        $(, [$($global_dependency:ident),*])?,
1016        |$($param:ident),+| $derive_fn:expr,
1017        impl_eq_hash = $impl_eq_hash:ident
1018        $(, $($extra:tt)+)*
1019    ) => {
1020        $crate::define_property!(
1021            @apply_property_decoration
1022            $impl_eq_hash,
1023            pub enum $name {
1024                $($variant),*
1025            },
1026            $name
1027        );
1028
1029        $crate::impl_derived_property!(
1030            $name,
1031            $entity,
1032            [$($dependency),*],
1033            [$($($global_dependency),*)?],
1034            |$($param),+| $derive_fn
1035            $(, $($extra)+)*
1036        );
1037    };
1038    (
1039        enum $name:ident {
1040            $($variant:ident),* $(,)?
1041        },
1042        $entity:ident,
1043        [$($dependency:ident),*]
1044        $(, [$($global_dependency:ident),*])?,
1045        |$($param:ident),+| $derive_fn:expr
1046        $(, $($extra:tt)+)*
1047    ) => {
1048        $crate::define_property!(
1049            @apply_property_decoration
1050            ,
1051            pub enum $name {
1052                $($variant),*
1053            },
1054            $name
1055        );
1056
1057        $crate::impl_derived_property!(
1058            $name,
1059            $entity,
1060            [$($dependency),*],
1061            [$($($global_dependency),*)?],
1062            |$($param),+| $derive_fn
1063            $(, $($extra)+)*
1064        );
1065    };
1066}
1067
1068/// Implements the [`Property`][crate::entity::property::Property] trait for an existing type as a derived property.
1069///
1070/// Accepts the same parameters as [`define_derived_property!`][macro@crate::define_derived_property], except the first parameter is the name of a
1071/// type assumed to already be declared rather than a type declaration. This is the derived property equivalent
1072/// of [`impl_property!`][macro@crate::impl_property]. It calls [`impl_property!`][macro@crate::impl_property] with the appropriate derived property parameters.
1073#[macro_export]
1074macro_rules! impl_derived_property {
1075    (
1076        $name:ident,
1077        $entity:ident,
1078        [$($dependency:ident),*]
1079        $(, [$($global_dependency:ident),*])?,
1080        |$($param:ident),+| $derive_fn:expr
1081        $(, $($extra:tt)+)*
1082    ) => {
1083        $crate::impl_property!(
1084            $name,
1085            $entity,
1086            compute_derived_fn = $crate::impl_derived_property!(
1087                @construct_compute_fn
1088                $entity,
1089                [$($dependency),*],
1090                [$($($global_dependency),*)?],
1091                |$($param),+| $derive_fn
1092            ),
1093            collect_deps_fn = | deps: &mut $crate::HashSet<usize> | {
1094                $(
1095                    if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() {
1096                        <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps);
1097                    } else {
1098                        deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id());
1099                    }
1100                )*
1101            }
1102            $(, $($extra)+)*
1103        );
1104    };
1105
1106    // Internal branch to construct the compute function.
1107    (
1108        @construct_compute_fn
1109        $entity:ident,
1110        [$($dependency:ident),*],
1111        [$($global_dependency:ident),*],
1112        |$($param:ident),+| $derive_fn:expr
1113    ) => {
1114        |context: &$crate::Context, entity_id| {
1115            #[allow(unused_imports)]
1116            use $crate::global_properties::ContextGlobalPropertiesExt;
1117            #[allow(unused_parens)]
1118            let ($($param,)*) = (
1119                $(context.get_property::<$entity, $dependency>(entity_id)),*,
1120                $(
1121                    context.get_global_property_value($global_dependency)
1122                        .expect(&format!("Global property {} not initialized", stringify!($global_dependency)))
1123                ),*
1124            );
1125            $derive_fn
1126        }
1127    };
1128
1129    (@unwrap_or $value:expr, $_default:expr) => { $value };
1130    (@unwrap_or, $default:expr) => { $default };
1131
1132    (
1133        @with_option_display_default
1134        $name:ident,
1135        $entity:ident,
1136        [$($dependency:ident),*],
1137        [$($global_dependency:ident),*],
1138        |$($param:ident),+| $derive_fn:expr
1139        $(, default_const = $default_const:expr)?
1140        $(, display_impl = $display_impl:expr)?
1141        $(, collect_deps_fn = $collect_deps_fn:expr)?
1142        $(, ctor_registration = $ctor_registration:expr)?
1143    ) => {
1144        $crate::impl_derived_property!(
1145            $name,
1146            $entity,
1147            [$($dependency),*],
1148            [$($global_dependency),*],
1149            |$($param),+| $derive_fn
1150            $(, default_const = $default_const)?
1151            , display_impl = $crate::impl_derived_property!(@unwrap_or $($display_impl)?, |value: &$name| {
1152                match value.0 {
1153                    Some(v) => format!("{:?}", v),
1154                    None => "None".to_string(),
1155                }
1156            })
1157            $(, collect_deps_fn = $collect_deps_fn)?
1158            $(, ctor_registration = $ctor_registration)?
1159        );
1160    };
1161
1162}
1163
1164/// Defines a derived property consisting of a (named) tuple of other properties. The primary use case
1165/// is for indexing and querying properties jointly.
1166///
1167/// The querying subsystem is able to detect when its multiple component properties are
1168/// equivalent to an indexed multi-property and use that index to perform the query.
1169///
1170/// Multi-properties must have at least two component properties:
1171///
1172/// ```
1173/// use ixa::{define_entity, define_property, define_multi_property};
1174/// define_entity!(Person);
1175/// define_property!(struct Age(u8), Person, default_const = Age(0));
1176/// define_property!(struct Height(u8), Person, default_const = Height(0));
1177/// define_multi_property!(Person, (Age, Height));
1178/// ```
1179///
1180/// ```compile_fail
1181/// use ixa::{define_entity, define_multi_property};
1182/// define_entity!(Person);
1183/// define_multi_property!(Person);
1184/// ```
1185///
1186/// ```compile_fail
1187/// use ixa::{define_entity, define_property, define_multi_property};
1188/// define_entity!(Person);
1189/// define_property!(struct Age(u8), Person, default_const = Age(0));
1190/// define_multi_property!(Person, (Age));
1191/// ```
1192///
1193/// Tuple-first and flat component syntax are not supported:
1194///
1195/// ```compile_fail
1196/// use ixa::{define_entity, define_property, define_multi_property};
1197/// define_entity!(Person);
1198/// define_property!(struct Age(u8), Person, default_const = Age(0));
1199/// define_property!(struct Height(u8), Person, default_const = Height(0));
1200/// define_multi_property!((Age, Height), Person);
1201/// ```
1202///
1203/// ```compile_fail
1204/// use ixa::{define_entity, define_property, define_multi_property};
1205/// define_entity!(Person);
1206/// define_property!(struct Age(u8), Person, default_const = Age(0));
1207/// define_property!(struct Height(u8), Person, default_const = Height(0));
1208/// define_multi_property!(Person, Age, Height);
1209/// ```
1210///
1211/// Components must be the underlying property type, not a type alias (see issue #843):
1212///
1213/// ```compile_fail
1214/// use ixa::{define_entity, define_property, define_multi_property};
1215/// define_entity!(Person);
1216/// define_property!(struct Age(u8), Person, default_const = Age(0));
1217/// define_property!(struct Height(u8), Person, default_const = Height(0));
1218/// type Years = Age;
1219/// define_multi_property!(Person, (Years, Height));
1220/// ```
1221#[macro_export]
1222macro_rules! define_multi_property {
1223        (@impl
1224            $entity:ident,
1225            $($dependency:ident),+
1226        ) => {
1227            $crate::paste::paste! {
1228                type [<$($dependency)*>] = ( $($dependency),+ );
1229
1230                // Reject type aliases; see issue #843.
1231                $(
1232                    const _: () = assert!(
1233                        $crate::entity::property::const_str_eq(
1234                            stringify!($dependency),
1235                            <$dependency as $crate::entity::property::Property<$entity>>::NAME,
1236                        ),
1237                        concat!(
1238                            "define_multi_property!: `",
1239                            stringify!($dependency),
1240                            "` is a type alias; use the underlying property type (see issue #843)."
1241                        ),
1242                    );
1243                )+
1244
1245                $crate::impl_property!(
1246                    @multi_property
1247                    [<$($dependency)*>],
1248                    $entity,
1249                    ( $($dependency),+ ),
1250                    compute_derived_fn = |context: &$crate::Context, entity_id: $crate::entity::EntityId<$entity>| {
1251                        (
1252                            $(<$crate::Context as $crate::entity::ContextEntitiesExt>::get_property::<$entity, $dependency>(
1253                                context,
1254                                entity_id,
1255                            )),+
1256                        )
1257                    },
1258
1259                    collect_deps_fn = | deps: &mut $crate::HashSet<usize> | {
1260                        $(
1261                            if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() {
1262                                <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps);
1263                            } else {
1264                                deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id());
1265                            }
1266                        )*
1267                    },
1268
1269                    display_impl = |val: &( $($dependency),+ )| {
1270                        let ( $( [<_ $dependency:lower>] ),+ ) = val;
1271                        let mut displayed = String::from("(");
1272                        $(
1273                            displayed.push_str(
1274                                &<$dependency as $crate::entity::property::Property<$entity>>::get_display([<_ $dependency:lower>])
1275                            );
1276                            displayed.push_str(", ");
1277                        )+
1278                        displayed.truncate(displayed.len() - 2);
1279                        displayed.push_str(")");
1280                        displayed
1281                    },
1282
1283                    ctor_registration = {
1284                        let mut type_ids = [$( <$dependency as $crate::entity::property::Property<$entity>>::type_id() ),+];
1285                        type_ids.sort_unstable();
1286                        if let Some((_, existing_name)) =
1287                            $crate::entity::multi_property::register_type_ids_to_multi_property_id(
1288                                <$entity as $crate::entity::Entity>::id(),
1289                                &type_ids,
1290                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::type_id(),
1291                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::id(),
1292                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1293                            )
1294                        {
1295                            $crate::entity::multi_property::record_pre_main_warning(format!(
1296                                "multi-property {} is equivalent to already registered multi-property {existing_name}; queries will resolve to {existing_name}, and attempting to index {} will panic",
1297                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1298                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1299                            ));
1300                        }
1301                        $crate::entity::property_store::add_to_property_registry::<$entity, [<$($dependency)*>]>();
1302                    }
1303                );
1304
1305            }
1306        };
1307
1308        (
1309            $entity:ident,
1310            ( $first:ident, $second:ident $(, $dependency:ident)* $(,)? )
1311            $(,)?
1312        ) => {
1313            $crate::define_multi_property!(@impl $entity, $first, $second $(, $dependency)*);
1314        };
1315
1316        () => {
1317            compile_error!(
1318                "define_multi_property!: expected `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1319            );
1320        };
1321
1322        ($entity:ident $(,)?) => {
1323            compile_error!(
1324                "define_multi_property!: multi-properties require at least two component properties"
1325            );
1326        };
1327
1328        (( $($dependency:ident),+ $(,)? ), $entity:ident $(,)?) => {
1329            compile_error!(
1330                "define_multi_property!: tuple-first syntax is no longer supported; write `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1331            );
1332        };
1333
1334        ($entity:ident, ( $only:ident $(,)? ) $(,)?) => {
1335            compile_error!(
1336                "define_multi_property!: multi-properties require at least two component properties"
1337            );
1338        };
1339
1340        (
1341            $entity:ident,
1342            $first:ident,
1343            $second:ident
1344            $(, $dependency:ident)*
1345            $(,)?
1346        ) => {
1347            compile_error!(
1348                "define_multi_property!: flat component syntax is no longer supported; write `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1349            );
1350        };
1351
1352        ($entity:ident, $only:ident $(,)?) => {
1353            compile_error!(
1354                "define_multi_property!: multi-properties require at least two component properties"
1355            );
1356        };
1357
1358        ($($tokens:tt)*) => {
1359            compile_error!(
1360                "define_multi_property!: expected `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1361            );
1362        };
1363    }
1364#[cfg(test)]
1365mod tests {
1366    // We define unused properties to test macro implementation.
1367    #![allow(dead_code)]
1368
1369    use crate::entity::QueryInternal;
1370    use crate::prelude::*;
1371    use crate::with;
1372
1373    define_entity!(Person);
1374    define_entity!(Group);
1375
1376    define_property!(struct Pu32(u32), Person, default_const = Pu32(0));
1377    define_property!(struct POu32(Option<u32>), Person, default_const = POu32(None));
1378    define_property!(
1379        struct POFloat(Option<f64>),
1380        Person,
1381        impl_eq_hash = both,
1382        default_const = POFloat(None)
1383    );
1384    define_property!(
1385        struct POu32Custom(Option<u32>),
1386        Person,
1387        default_const = POu32Custom(None),
1388        display_impl = |value: &POu32Custom| match value.0 {
1389            Some(v) => format!("custom:{v}"),
1390            None => "custom:none".to_string(),
1391        }
1392    );
1393    define_property!(struct Name(&'static str), Person, default_const = Name(""));
1394    define_property!(struct Age(u8), Person, default_const = Age(0));
1395    define_property!(struct Weight(f64), Person, impl_eq_hash = both, default_const = Weight(0.0));
1396
1397    // A struct with named fields
1398    define_property!(
1399        struct Innocculation {
1400            time: f64,
1401            dose: u8,
1402        },
1403        Person,
1404        impl_eq_hash = both,
1405        default_const = Innocculation { time: 0.0, dose: 0 }
1406    );
1407
1408    // An enum non-derived property
1409    define_property!(
1410        enum InfectionStatus {
1411            Susceptible,
1412            Infected,
1413            Recovered,
1414        },
1415        Person,
1416        default_const = InfectionStatus::Susceptible
1417    );
1418
1419    // An enum derived property
1420    define_derived_property!(
1421        enum AgeGroup {
1422            Child,
1423            Adult,
1424            Senior,
1425        },
1426        Person,
1427        [Age], // Depends only on age
1428        |age| {
1429            let age: Age = age;
1430            if age.0 < 18 {
1431                AgeGroup::Child
1432            } else if age.0 < 65 {
1433                AgeGroup::Adult
1434            } else {
1435                AgeGroup::Senior
1436            }
1437        }
1438    );
1439
1440    // Derived property - computed from other properties
1441    define_derived_property!(struct DerivedProp(bool), Person, [Age],
1442        |age| {
1443            DerivedProp(age.0 % 2 == 0)
1444        }
1445    );
1446
1447    define_derived_property!(
1448        struct DerivedMaybeAge(Option<u8>),
1449        Person,
1450        [Age],
1451        |age| DerivedMaybeAge((age.0 != 0).then_some(age.0))
1452    );
1453
1454    define_derived_property!(
1455        struct DerivedMaybeWeight(Option<f64>),
1456        Person,
1457        [Age],
1458        |age| DerivedMaybeWeight((age.0 != 0).then_some(age.0 as f64)),
1459        impl_eq_hash = both
1460    );
1461
1462    define_derived_property!(
1463        struct DerivedMaybeAgeCustom(Option<u8>),
1464        Person,
1465        [Age],
1466        |age| DerivedMaybeAgeCustom((age.0 != 0).then_some(age.0)),
1467        display_impl = |value: &DerivedMaybeAgeCustom| match value.0 {
1468            Some(v) => format!("derived:{v}"),
1469            None => "derived:none".to_string(),
1470        }
1471    );
1472
1473    define_derived_property!(
1474        struct DerivedWeight(f64),
1475        Person,
1476        [Age],
1477        |age| DerivedWeight(age.0 as f64),
1478        impl_eq_hash = both
1479    );
1480
1481    #[derive(Debug, PartialEq, Clone, Copy)]
1482    struct NonIndexableFloat(f64);
1483    impl_property!(
1484        NonIndexableFloat,
1485        Person,
1486        default_const = NonIndexableFloat(0.0)
1487    );
1488
1489    // A property type for two distinct entities.
1490    #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
1491    pub enum InfectionKind {
1492        Respiratory,
1493        Genetic,
1494        Superficial,
1495    }
1496    impl_property!(
1497        InfectionKind,
1498        Person,
1499        default_const = InfectionKind::Respiratory
1500    );
1501    impl_property!(InfectionKind, Group, default_const = InfectionKind::Genetic);
1502
1503    define_multi_property!(Person, (Name, Age, Weight));
1504    define_multi_property!(Person, (Age, Weight, Name));
1505    define_multi_property!(Person, (Weight, Age, Name));
1506    define_multi_property!(Person, (Name, Weight,),);
1507
1508    // For convenience
1509    type ProfileNAW = (Name, Age, Weight);
1510    type ProfileAWN = (Age, Weight, Name);
1511    type ProfileWAN = (Weight, Age, Name);
1512    type ProfileNW = (Name, Weight);
1513
1514    define_entity!(SingleProfilePerson);
1515    define_property!(
1516        struct SingleName(&'static str),
1517        SingleProfilePerson,
1518        default_const = SingleName("")
1519    );
1520    define_property!(
1521        struct SingleAge(u8),
1522        SingleProfilePerson,
1523        default_const = SingleAge(0)
1524    );
1525    define_property!(
1526        struct SingleWeight(u8),
1527        SingleProfilePerson,
1528        default_const = SingleWeight(0)
1529    );
1530    define_multi_property!(SingleProfilePerson, (SingleName, SingleAge, SingleWeight));
1531    type SingleProfile = (SingleName, SingleAge, SingleWeight);
1532
1533    #[test]
1534    fn test_multi_property_ordering() {
1535        let a = (Name("Jane"), Age(22), Weight(180.5));
1536        let b = (Age(22), Weight(180.5), Name("Jane"));
1537        let c = (Weight(180.5), Age(22), Name("Jane"));
1538
1539        // Equivalent multi-properties keep distinct storage and type identities.
1540        // Query routing equivalence is handled by the multi-property registry.
1541        assert_ne!(ProfileNAW::id(), ProfileAWN::id());
1542        assert_ne!(ProfileNAW::id(), ProfileWAN::id());
1543        assert_ne!(ProfileNAW::type_id(), ProfileAWN::type_id());
1544        assert_ne!(ProfileNAW::type_id(), ProfileWAN::type_id());
1545        let _ = ProfileNW::id();
1546
1547        let query_parts = ProfileNAW::query_parts_for_value(&a);
1548        assert_eq!(
1549            ProfileAWN::value_from_query_parts(query_parts.as_ref()),
1550            Some(b)
1551        );
1552        assert_eq!(
1553            ProfileWAN::value_from_query_parts(query_parts.as_ref()),
1554            Some(c)
1555        );
1556    }
1557
1558    #[test]
1559    fn test_non_indexable_property_unindexed_behavior() {
1560        let mut context = Context::new();
1561
1562        let first = context
1563            .add_entity(with!(Person, NonIndexableFloat(1.5)))
1564            .unwrap();
1565        let _second = context
1566            .add_entity(with!(Person, NonIndexableFloat(2.5)))
1567            .unwrap();
1568
1569        assert_eq!(
1570            context.get_property::<Person, NonIndexableFloat>(first),
1571            NonIndexableFloat(1.5)
1572        );
1573
1574        context.set_property(first, NonIndexableFloat(3.5));
1575        assert_eq!(
1576            context.get_property::<Person, NonIndexableFloat>(first),
1577            NonIndexableFloat(3.5)
1578        );
1579
1580        let mut results = Vec::new();
1581        context.with_query_results(with!(Person, NonIndexableFloat(3.5)), &mut |entity_ids| {
1582            results = entity_ids.into_iter().collect::<Vec<_>>();
1583        });
1584        assert_eq!(results, vec![first]);
1585        assert_eq!(
1586            context.query_entity_count(with!(Person, NonIndexableFloat(3.5))),
1587            1
1588        );
1589    }
1590
1591    #[test]
1592    fn test_single_multi_property_vs_property_query() {
1593        let mut context = Context::new();
1594
1595        context
1596            .add_entity(with!(
1597                SingleProfilePerson,
1598                SingleName("John"),
1599                SingleAge(42),
1600                SingleWeight(220)
1601            ))
1602            .unwrap();
1603        context
1604            .add_entity(with!(
1605                SingleProfilePerson,
1606                SingleName("Jane"),
1607                SingleAge(22),
1608                SingleWeight(180)
1609            ))
1610            .unwrap();
1611        context
1612            .add_entity(with!(
1613                SingleProfilePerson,
1614                SingleName("Bob"),
1615                SingleAge(32),
1616                SingleWeight(190)
1617            ))
1618            .unwrap();
1619        context
1620            .add_entity(with!(
1621                SingleProfilePerson,
1622                SingleName("Alice"),
1623                SingleAge(22),
1624                SingleWeight(170)
1625            ))
1626            .unwrap();
1627
1628        context.index_property::<SingleProfilePerson, SingleProfile>();
1629
1630        let example_query = (SingleName("Alice"), SingleAge(22), SingleWeight(170));
1631        assert_eq!(
1632            <SingleProfile as QueryInternal<SingleProfilePerson>>::multi_property_id(
1633                &example_query
1634            ),
1635            Some(SingleProfile::id())
1636        );
1637        let query_parts = QueryInternal::query_parts(&example_query);
1638        assert_eq!(
1639            SingleProfile::value_from_query_parts(query_parts.as_ref()),
1640            Some((SingleName("Alice"), SingleAge(22), SingleWeight(170)))
1641        );
1642
1643        context.with_query_results(
1644            with!(
1645                SingleProfilePerson,
1646                (SingleName("John"), SingleAge(42), SingleWeight(220))
1647            ),
1648            &mut |results| {
1649                assert_eq!(results.into_iter().count(), 1);
1650            },
1651        );
1652    }
1653
1654    #[test]
1655    fn test_equivalent_multi_property_query_routing() {
1656        let example_query = (Name("Alice"), Age(22), Weight(170.5));
1657        let query_multi_property_id =
1658            <(Name, Age, Weight) as QueryInternal<Person>>::multi_property_id(&example_query);
1659
1660        assert!(matches!(
1661            query_multi_property_id,
1662            Some(id) if id == ProfileNAW::id() || id == ProfileAWN::id() || id == ProfileWAN::id()
1663        ));
1664
1665        let query_parts = QueryInternal::query_parts(&example_query);
1666        assert_eq!(
1667            ProfileNAW::value_from_query_parts(query_parts.as_ref()),
1668            Some((Name("Alice"), Age(22), Weight(170.5)))
1669        );
1670        assert_eq!(
1671            ProfileAWN::value_from_query_parts(query_parts.as_ref()),
1672            Some((Age(22), Weight(170.5), Name("Alice")))
1673        );
1674        assert_eq!(
1675            ProfileWAN::value_from_query_parts(query_parts.as_ref()),
1676            Some((Weight(170.5), Age(22), Name("Alice")))
1677        );
1678    }
1679
1680    #[test]
1681    fn test_derived_property() {
1682        let mut context = Context::new();
1683
1684        let senior = context
1685            .add_entity::<Person, _>(with!(Person, Age(92)))
1686            .unwrap();
1687        let child = context
1688            .add_entity::<Person, _>(with!(Person, Age(12)))
1689            .unwrap();
1690        let adult = context
1691            .add_entity::<Person, _>(with!(Person, Age(44)))
1692            .unwrap();
1693
1694        let senior_group: AgeGroup = context.get_property(senior);
1695        let child_group: AgeGroup = context.get_property(child);
1696        let adult_group: AgeGroup = context.get_property(adult);
1697
1698        assert_eq!(senior_group, AgeGroup::Senior);
1699        assert_eq!(child_group, AgeGroup::Child);
1700        assert_eq!(adult_group, AgeGroup::Adult);
1701
1702        // Age has no dependencies (only dependents)
1703        assert!(Age::non_derived_dependencies().is_empty());
1704        // AgeGroup depends only on Age
1705        assert_eq!(AgeGroup::non_derived_dependencies(), [Age::id()]);
1706
1707        // Age has several dependents. This assert may break if you add or remove the properties in this test module.
1708        let mut expected_dependents = [
1709            AgeGroup::id(),
1710            DerivedProp::id(),
1711            DerivedMaybeAge::id(),
1712            DerivedMaybeWeight::id(),
1713            DerivedMaybeAgeCustom::id(),
1714            DerivedWeight::id(),
1715            ProfileNAW::id(),
1716            ProfileAWN::id(),
1717            ProfileWAN::id(),
1718        ];
1719        expected_dependents.sort_unstable();
1720        assert_eq!(Age::dependents(), expected_dependents);
1721    }
1722
1723    #[test]
1724    fn test_get_display() {
1725        let mut context = Context::new();
1726        let person = context
1727            .add_entity(with!(Person, POu32(Some(42)), Pu32(22)))
1728            .unwrap();
1729        assert_eq!(
1730            POu32::get_display(&context.get_property::<_, POu32>(person)).to_string(),
1731            "42"
1732        );
1733        assert_eq!(
1734            Pu32::get_display(&context.get_property::<_, Pu32>(person)).to_string(),
1735            "Pu32(22)"
1736        );
1737        let person2 = context
1738            .add_entity(with!(Person, POu32(None), Pu32(11)))
1739            .unwrap();
1740        assert_eq!(
1741            POu32::get_display(&context.get_property::<_, POu32>(person2)).to_string(),
1742            "None"
1743        );
1744    }
1745
1746    #[test]
1747    fn test_option_property_display_patterns() {
1748        let mut context = Context::new();
1749
1750        let some_person = context
1751            .add_entity(with!(
1752                Person,
1753                POu32(Some(42)),
1754                POFloat(Some(3.5)),
1755                POu32Custom(Some(7)),
1756                Pu32(1),
1757            ))
1758            .unwrap();
1759        let none_person = context
1760            .add_entity(with!(
1761                Person,
1762                POu32(None),
1763                POFloat(None),
1764                POu32Custom(None),
1765                Pu32(2)
1766            ))
1767            .unwrap();
1768
1769        assert_eq!(
1770            POu32::get_display(&context.get_property::<_, POu32>(some_person)),
1771            "42"
1772        );
1773        assert_eq!(
1774            POu32::get_display(&context.get_property::<_, POu32>(none_person)),
1775            "None"
1776        );
1777
1778        assert_eq!(
1779            POFloat::get_display(&context.get_property::<_, POFloat>(some_person)),
1780            "3.5"
1781        );
1782        assert_eq!(
1783            POFloat::get_display(&context.get_property::<_, POFloat>(none_person)),
1784            "None"
1785        );
1786
1787        assert_eq!(
1788            POu32Custom::get_display(&context.get_property::<_, POu32Custom>(some_person)),
1789            "custom:7"
1790        );
1791        assert_eq!(
1792            POu32Custom::get_display(&context.get_property::<_, POu32Custom>(none_person)),
1793            "custom:none"
1794        );
1795    }
1796
1797    #[test]
1798    fn test_option_derived_property_display_patterns() {
1799        let mut context = Context::new();
1800
1801        let some_person = context
1802            .add_entity::<Person, _>(with!(Person, Age(42)))
1803            .unwrap();
1804        let none_person = context
1805            .add_entity::<Person, _>(with!(Person, Age(0)))
1806            .unwrap();
1807
1808        assert_eq!(
1809            DerivedMaybeAge::get_display(&context.get_property::<_, DerivedMaybeAge>(some_person)),
1810            "42"
1811        );
1812        assert_eq!(
1813            DerivedMaybeAge::get_display(&context.get_property::<_, DerivedMaybeAge>(none_person)),
1814            "None"
1815        );
1816
1817        assert_eq!(
1818            DerivedMaybeWeight::get_display(
1819                &context.get_property::<_, DerivedMaybeWeight>(some_person)
1820            ),
1821            "42.0"
1822        );
1823        assert_eq!(
1824            DerivedMaybeWeight::get_display(
1825                &context.get_property::<_, DerivedMaybeWeight>(none_person)
1826            ),
1827            "None"
1828        );
1829
1830        assert_eq!(
1831            DerivedMaybeAgeCustom::get_display(
1832                &context.get_property::<_, DerivedMaybeAgeCustom>(some_person)
1833            ),
1834            "derived:42"
1835        );
1836        assert_eq!(
1837            DerivedMaybeAgeCustom::get_display(
1838                &context.get_property::<_, DerivedMaybeAgeCustom>(none_person)
1839            ),
1840            "derived:none"
1841        );
1842    }
1843
1844    #[test]
1845    fn test_debug_trait() {
1846        let property = Pu32(11);
1847        let debug_str = format!("{:?}", property);
1848        assert_eq!(debug_str, "Pu32(11)");
1849
1850        let property = POu32(Some(22));
1851        let debug_str = format!("{:?}", property);
1852        assert_eq!(debug_str, "POu32(Some(22))");
1853    }
1854
1855    #[test]
1856    fn test_define_derived_property_impl_eq_hash() {
1857        let mut values = crate::HashSet::default();
1858        values.insert(DerivedWeight(3.0));
1859        assert!(values.contains(&DerivedWeight(3.0)));
1860    }
1861}