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 collect_non_derived_dependencies(result: &mut $crate::HashSet<usize>) {
806                ($collect_deps_fn)(result)
807            }
808        }
809
810        $crate::paste::paste! {
811            $crate::ctor::declarative::ctor!{
812                #[ctor(unsafe)]
813                fn [<_register_property_ $entity:snake _ $property:snake>]() {
814                    $ctor_registration
815                }
816            }
817        }
818    };
819}
820
821/// The "derived" variant of [`define_property!`][macro@crate::define_property] for defining simple derived property types.
822/// Defines a `struct` or `enum` with a standard set of derives and automatically invokes
823/// [`impl_derived_property!`][macro@crate::impl_derived_property] for it.
824///
825/// Defines a derived property with the following parameters:
826/// * Property type declaration: A struct or enum declaration.
827/// * `$entity`: The name of the entity of which the new type is a property.
828/// * `[$($dependency),+]`: A list of person properties the derived property depends on.
829/// * `[$(global_dependency),*]`: A list of global properties the derived property depends on. Can optionally be omitted if empty.
830/// * `$calculate`: A closure that takes the values of each dependency and returns the derived value.
831/// * Optional parameters: The same optional parameters accepted by [`impl_property!`][macro@crate::impl_property],
832///   plus `impl_eq_hash = Eq | Hash | both | neither` to control whether `Eq`/`Hash` are derived or generated
833///   for the declared type, mirroring [`define_property!`][macro@crate::define_property].
834#[macro_export]
835macro_rules! define_derived_property {
836    // Implementation Notes
837    //
838    // We reuse `define_property!`'s shared decoration helper, then delegate the derived-property
839    // behavior to `impl_derived_property!`.
840    //
841    // See `derive_property!` implementation notes for why each type form is duplicated.
842
843    // Struct (tuple) with single Option<T> field
844    (
845        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
846        $entity:ident,
847        [$($dependency:ident),*]
848        $(, [$($global_dependency:ident),*])?,
849        |$($param:ident),+| $derive_fn:expr,
850        impl_eq_hash = $impl_eq_hash:ident
851        $(, $($extra:tt)+)*
852    ) => {
853        $crate::define_property!(
854            @apply_property_decoration
855            $impl_eq_hash,
856            pub struct $name(pub Option<$inner_ty>);,
857            $name
858        );
859
860        $crate::impl_derived_property!(
861            @with_option_display_default
862            $name,
863            $entity,
864            [$($dependency),*],
865            [$($($global_dependency),*)?],
866            |$($param),+| $derive_fn
867            $(, $($extra)+)*
868        );
869    };
870    (
871        struct $name:ident ( $visibility:vis Option<$inner_ty:ty> ),
872        $entity:ident,
873        [$($dependency:ident),*]
874        $(, [$($global_dependency:ident),*])?,
875        |$($param:ident),+| $derive_fn:expr
876        $(, $($extra:tt)+)*
877    ) => {
878        $crate::define_property!(
879            @apply_property_decoration
880            ,
881            pub struct $name(pub Option<$inner_ty>);,
882            $name
883        );
884
885        $crate::impl_derived_property!(
886            @with_option_display_default
887            $name,
888            $entity,
889            [$($dependency),*],
890            [$($($global_dependency),*)?],
891            |$($param),+| $derive_fn
892            $(, $($extra)+)*
893        );
894    };
895
896    // Struct (tuple)
897    (
898        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
899        $entity:ident,
900        [$($dependency:ident),*]
901        $(, [$($global_dependency:ident),*])?,
902        |$($param:ident),+| $derive_fn:expr,
903        impl_eq_hash = $impl_eq_hash:ident
904        $(, $($extra:tt)+)*
905    ) => {
906        $crate::define_property!(
907            @apply_property_decoration
908            $impl_eq_hash,
909            pub struct $name( $(pub $field_ty),* );,
910            $name
911        );
912
913        $crate::impl_derived_property!(
914            $name,
915            $entity,
916            [$($dependency),*],
917            [$($($global_dependency),*)?],
918            |$($param),+| $derive_fn
919            $(, $($extra)+)*
920        );
921    };
922    (
923        struct $name:ident ( $($visibility:vis $field_ty:ty),* $(,)? ),
924        $entity:ident,
925        [$($dependency:ident),*]
926        $(, [$($global_dependency:ident),*])?,
927        |$($param:ident),+| $derive_fn:expr
928        $(, $($extra:tt)+)*
929    ) => {
930        $crate::define_property!(
931            @apply_property_decoration
932            ,
933            pub struct $name( $(pub $field_ty),* );,
934            $name
935        );
936
937        $crate::impl_derived_property!(
938            $name,
939            $entity,
940            [$($dependency),*],
941            [$($($global_dependency),*)?],
942            |$($param),+| $derive_fn
943            $(, $($extra)+)*
944        );
945    };
946
947    // Struct (named fields)
948    (
949        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
950        $entity:ident,
951        [$($dependency:ident),*]
952        $(, [$($global_dependency:ident),*])?,
953        |$($param:ident),+| $derive_fn:expr,
954        impl_eq_hash = $impl_eq_hash:ident
955        $(, $($extra:tt)+)*
956    ) => {
957        $crate::define_property!(
958            @apply_property_decoration
959            $impl_eq_hash,
960            pub struct $name { $($visibility $field_name : $field_ty),* },
961            $name
962        );
963
964        $crate::impl_derived_property!(
965            $name,
966            $entity,
967            [$($dependency),*],
968            [$($($global_dependency),*)?],
969            |$($param),+| $derive_fn
970            $(, $($extra)+)*
971        );
972    };
973    (
974        struct $name:ident { $($visibility:vis $field_name:ident : $field_ty:ty),* $(,)? },
975        $entity:ident,
976        [$($dependency:ident),*]
977        $(, [$($global_dependency:ident),*])?,
978        |$($param:ident),+| $derive_fn:expr
979        $(, $($extra:tt)+)*
980    ) => {
981        $crate::define_property!(
982            @apply_property_decoration
983            ,
984            pub struct $name { $($visibility $field_name : $field_ty),* },
985            $name
986        );
987
988        $crate::impl_derived_property!(
989            $name,
990            $entity,
991            [$($dependency),*],
992            [$($($global_dependency),*)?],
993            |$($param),+| $derive_fn
994            $(, $($extra)+)*
995        );
996    };
997
998    // Enum
999    (
1000        enum $name:ident {
1001            $($variant:ident),* $(,)?
1002        },
1003        $entity:ident,
1004        [$($dependency:ident),*]
1005        $(, [$($global_dependency:ident),*])?,
1006        |$($param:ident),+| $derive_fn:expr,
1007        impl_eq_hash = $impl_eq_hash:ident
1008        $(, $($extra:tt)+)*
1009    ) => {
1010        $crate::define_property!(
1011            @apply_property_decoration
1012            $impl_eq_hash,
1013            pub enum $name {
1014                $($variant),*
1015            },
1016            $name
1017        );
1018
1019        $crate::impl_derived_property!(
1020            $name,
1021            $entity,
1022            [$($dependency),*],
1023            [$($($global_dependency),*)?],
1024            |$($param),+| $derive_fn
1025            $(, $($extra)+)*
1026        );
1027    };
1028    (
1029        enum $name:ident {
1030            $($variant:ident),* $(,)?
1031        },
1032        $entity:ident,
1033        [$($dependency:ident),*]
1034        $(, [$($global_dependency:ident),*])?,
1035        |$($param:ident),+| $derive_fn:expr
1036        $(, $($extra:tt)+)*
1037    ) => {
1038        $crate::define_property!(
1039            @apply_property_decoration
1040            ,
1041            pub enum $name {
1042                $($variant),*
1043            },
1044            $name
1045        );
1046
1047        $crate::impl_derived_property!(
1048            $name,
1049            $entity,
1050            [$($dependency),*],
1051            [$($($global_dependency),*)?],
1052            |$($param),+| $derive_fn
1053            $(, $($extra)+)*
1054        );
1055    };
1056}
1057
1058/// Implements the [`Property`][crate::entity::property::Property] trait for an existing type as a derived property.
1059///
1060/// Accepts the same parameters as [`define_derived_property!`][macro@crate::define_derived_property], except the first parameter is the name of a
1061/// type assumed to already be declared rather than a type declaration. This is the derived property equivalent
1062/// of [`impl_property!`][macro@crate::impl_property]. It calls [`impl_property!`][macro@crate::impl_property] with the appropriate derived property parameters.
1063#[macro_export]
1064macro_rules! impl_derived_property {
1065    (
1066        $name:ident,
1067        $entity:ident,
1068        [$($dependency:ident),*]
1069        $(, [$($global_dependency:ident),*])?,
1070        |$($param:ident),+| $derive_fn:expr
1071        $(, $($extra:tt)+)*
1072    ) => {
1073        $crate::impl_property!(
1074            $name,
1075            $entity,
1076            compute_derived_fn = $crate::impl_derived_property!(
1077                @construct_compute_fn
1078                $entity,
1079                [$($dependency),*],
1080                [$($($global_dependency),*)?],
1081                |$($param),+| $derive_fn
1082            ),
1083            collect_deps_fn = | deps: &mut $crate::HashSet<usize> | {
1084                $(
1085                    if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() {
1086                        <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps);
1087                    } else {
1088                        deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id());
1089                    }
1090                )*
1091            }
1092            $(, $($extra)+)*
1093        );
1094    };
1095
1096    // Internal branch to construct the compute function.
1097    (
1098        @construct_compute_fn
1099        $entity:ident,
1100        [$($dependency:ident),*],
1101        [$($global_dependency:ident),*],
1102        |$($param:ident),+| $derive_fn:expr
1103    ) => {
1104        |context: &$crate::Context, entity_id| {
1105            #[allow(unused_imports)]
1106            use $crate::global_properties::ContextGlobalPropertiesExt;
1107            #[allow(unused_parens)]
1108            let ($($param,)*) = (
1109                $(context.get_property::<$entity, $dependency>(entity_id)),*,
1110                $(
1111                    context.get_global_property_value($global_dependency)
1112                        .expect(&format!("Global property {} not initialized", stringify!($global_dependency)))
1113                ),*
1114            );
1115            $derive_fn
1116        }
1117    };
1118
1119    (@unwrap_or $value:expr, $_default:expr) => { $value };
1120    (@unwrap_or, $default:expr) => { $default };
1121
1122    (
1123        @with_option_display_default
1124        $name:ident,
1125        $entity:ident,
1126        [$($dependency:ident),*],
1127        [$($global_dependency:ident),*],
1128        |$($param:ident),+| $derive_fn:expr
1129        $(, default_const = $default_const:expr)?
1130        $(, display_impl = $display_impl:expr)?
1131        $(, collect_deps_fn = $collect_deps_fn:expr)?
1132        $(, ctor_registration = $ctor_registration:expr)?
1133    ) => {
1134        $crate::impl_derived_property!(
1135            $name,
1136            $entity,
1137            [$($dependency),*],
1138            [$($global_dependency),*],
1139            |$($param),+| $derive_fn
1140            $(, default_const = $default_const)?
1141            , display_impl = $crate::impl_derived_property!(@unwrap_or $($display_impl)?, |value: &$name| {
1142                match value.0 {
1143                    Some(v) => format!("{:?}", v),
1144                    None => "None".to_string(),
1145                }
1146            })
1147            $(, collect_deps_fn = $collect_deps_fn)?
1148            $(, ctor_registration = $ctor_registration)?
1149        );
1150    };
1151
1152}
1153
1154/// Defines a derived property consisting of a (named) tuple of other properties. The primary use case
1155/// is for indexing and querying properties jointly.
1156///
1157/// The querying subsystem is able to detect when its multiple component properties are
1158/// equivalent to an indexed multi-property and use that index to perform the query.
1159///
1160/// Multi-properties must have at least two component properties:
1161///
1162/// ```
1163/// use ixa::{define_entity, define_property, define_multi_property};
1164/// define_entity!(Person);
1165/// define_property!(struct Age(u8), Person, default_const = Age(0));
1166/// define_property!(struct Height(u8), Person, default_const = Height(0));
1167/// define_multi_property!(Person, (Age, Height));
1168/// ```
1169///
1170/// ```compile_fail
1171/// use ixa::{define_entity, define_multi_property};
1172/// define_entity!(Person);
1173/// define_multi_property!(Person);
1174/// ```
1175///
1176/// ```compile_fail
1177/// use ixa::{define_entity, define_property, define_multi_property};
1178/// define_entity!(Person);
1179/// define_property!(struct Age(u8), Person, default_const = Age(0));
1180/// define_multi_property!(Person, (Age));
1181/// ```
1182///
1183/// Tuple-first and flat component syntax are not supported:
1184///
1185/// ```compile_fail
1186/// use ixa::{define_entity, define_property, define_multi_property};
1187/// define_entity!(Person);
1188/// define_property!(struct Age(u8), Person, default_const = Age(0));
1189/// define_property!(struct Height(u8), Person, default_const = Height(0));
1190/// define_multi_property!((Age, Height), Person);
1191/// ```
1192///
1193/// ```compile_fail
1194/// use ixa::{define_entity, define_property, define_multi_property};
1195/// define_entity!(Person);
1196/// define_property!(struct Age(u8), Person, default_const = Age(0));
1197/// define_property!(struct Height(u8), Person, default_const = Height(0));
1198/// define_multi_property!(Person, Age, Height);
1199/// ```
1200///
1201/// Components must be the underlying property type, not a type alias (see issue #843):
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/// type Years = Age;
1209/// define_multi_property!(Person, (Years, Height));
1210/// ```
1211#[macro_export]
1212macro_rules! define_multi_property {
1213        (@impl
1214            $entity:ident,
1215            $($dependency:ident),+
1216        ) => {
1217            $crate::paste::paste! {
1218                type [<$($dependency)*>] = ( $($dependency),+ );
1219
1220                // Reject type aliases; see issue #843.
1221                $(
1222                    const _: () = assert!(
1223                        $crate::entity::property::const_str_eq(
1224                            stringify!($dependency),
1225                            <$dependency as $crate::entity::property::Property<$entity>>::NAME,
1226                        ),
1227                        concat!(
1228                            "define_multi_property!: `",
1229                            stringify!($dependency),
1230                            "` is a type alias; use the underlying property type (see issue #843)."
1231                        ),
1232                    );
1233                )+
1234
1235                $crate::impl_property!(
1236                    @multi_property
1237                    [<$($dependency)*>],
1238                    $entity,
1239                    ( $($dependency),+ ),
1240                    compute_derived_fn = |context: &$crate::Context, entity_id: $crate::entity::EntityId<$entity>| {
1241                        (
1242                            $(<$crate::Context as $crate::entity::ContextEntitiesExt>::get_property::<$entity, $dependency>(
1243                                context,
1244                                entity_id,
1245                            )),+
1246                        )
1247                    },
1248
1249                    collect_deps_fn = | deps: &mut $crate::HashSet<usize> | {
1250                        $(
1251                            if <$dependency as $crate::entity::property::Property<$entity>>::is_derived() {
1252                                <$dependency as $crate::entity::property::Property<$entity>>::collect_non_derived_dependencies(deps);
1253                            } else {
1254                                deps.insert(<$dependency as $crate::entity::property::Property<$entity>>::id());
1255                            }
1256                        )*
1257                    },
1258
1259                    display_impl = |val: &( $($dependency),+ )| {
1260                        let ( $( [<_ $dependency:lower>] ),+ ) = val;
1261                        let mut displayed = String::from("(");
1262                        $(
1263                            displayed.push_str(
1264                                &<$dependency as $crate::entity::property::Property<$entity>>::get_display([<_ $dependency:lower>])
1265                            );
1266                            displayed.push_str(", ");
1267                        )+
1268                        displayed.truncate(displayed.len() - 2);
1269                        displayed.push_str(")");
1270                        displayed
1271                    },
1272
1273                    ctor_registration = {
1274                        let mut type_ids = [$( <$dependency as $crate::entity::property::Property<$entity>>::type_id() ),+];
1275                        type_ids.sort_unstable();
1276                        if let Some((_, existing_name)) =
1277                            $crate::entity::multi_property::register_type_ids_to_multi_property_id(
1278                                <$entity as $crate::entity::Entity>::id(),
1279                                &type_ids,
1280                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::type_id(),
1281                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::id(),
1282                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1283                            )
1284                        {
1285                            $crate::entity::multi_property::record_pre_main_warning(format!(
1286                                "multi-property {} is equivalent to already registered multi-property {existing_name}; queries will resolve to {existing_name}, and attempting to index {} will panic",
1287                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1288                                <[<$($dependency)*>] as $crate::entity::property::Property<$entity>>::name(),
1289                            ));
1290                        }
1291                        $crate::entity::property_store::add_to_property_registry::<$entity, [<$($dependency)*>]>();
1292                    }
1293                );
1294
1295            }
1296        };
1297
1298        (
1299            $entity:ident,
1300            ( $first:ident, $second:ident $(, $dependency:ident)* $(,)? )
1301            $(,)?
1302        ) => {
1303            $crate::define_multi_property!(@impl $entity, $first, $second $(, $dependency)*);
1304        };
1305
1306        () => {
1307            compile_error!(
1308                "define_multi_property!: expected `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1309            );
1310        };
1311
1312        ($entity:ident $(,)?) => {
1313            compile_error!(
1314                "define_multi_property!: multi-properties require at least two component properties"
1315            );
1316        };
1317
1318        (( $($dependency:ident),+ $(,)? ), $entity:ident $(,)?) => {
1319            compile_error!(
1320                "define_multi_property!: tuple-first syntax is no longer supported; write `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1321            );
1322        };
1323
1324        ($entity:ident, ( $only:ident $(,)? ) $(,)?) => {
1325            compile_error!(
1326                "define_multi_property!: multi-properties require at least two component properties"
1327            );
1328        };
1329
1330        (
1331            $entity:ident,
1332            $first:ident,
1333            $second:ident
1334            $(, $dependency:ident)*
1335            $(,)?
1336        ) => {
1337            compile_error!(
1338                "define_multi_property!: flat component syntax is no longer supported; write `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1339            );
1340        };
1341
1342        ($entity:ident, $only:ident $(,)?) => {
1343            compile_error!(
1344                "define_multi_property!: multi-properties require at least two component properties"
1345            );
1346        };
1347
1348        ($($tokens:tt)*) => {
1349            compile_error!(
1350                "define_multi_property!: expected `define_multi_property!(Entity, (PropertyA, PropertyB, ...))`"
1351            );
1352        };
1353    }
1354#[cfg(test)]
1355mod tests {
1356    // We define unused properties to test macro implementation.
1357    #![allow(dead_code)]
1358
1359    use crate::entity::QueryInternal;
1360    use crate::prelude::*;
1361    use crate::with;
1362
1363    define_entity!(Person);
1364    define_entity!(Group);
1365
1366    define_property!(struct Pu32(u32), Person, default_const = Pu32(0));
1367    define_property!(struct POu32(Option<u32>), Person, default_const = POu32(None));
1368    define_property!(
1369        struct POFloat(Option<f64>),
1370        Person,
1371        impl_eq_hash = both,
1372        default_const = POFloat(None)
1373    );
1374    define_property!(
1375        struct POu32Custom(Option<u32>),
1376        Person,
1377        default_const = POu32Custom(None),
1378        display_impl = |value: &POu32Custom| match value.0 {
1379            Some(v) => format!("custom:{v}"),
1380            None => "custom:none".to_string(),
1381        }
1382    );
1383    define_property!(struct Name(&'static str), Person, default_const = Name(""));
1384    define_property!(struct Age(u8), Person, default_const = Age(0));
1385    define_property!(struct Weight(f64), Person, impl_eq_hash = both, default_const = Weight(0.0));
1386
1387    // A struct with named fields
1388    define_property!(
1389        struct Innocculation {
1390            time: f64,
1391            dose: u8,
1392        },
1393        Person,
1394        impl_eq_hash = both,
1395        default_const = Innocculation { time: 0.0, dose: 0 }
1396    );
1397
1398    // An enum non-derived property
1399    define_property!(
1400        enum InfectionStatus {
1401            Susceptible,
1402            Infected,
1403            Recovered,
1404        },
1405        Person,
1406        default_const = InfectionStatus::Susceptible
1407    );
1408
1409    // An enum derived property
1410    define_derived_property!(
1411        enum AgeGroup {
1412            Child,
1413            Adult,
1414            Senior,
1415        },
1416        Person,
1417        [Age], // Depends only on age
1418        |age| {
1419            let age: Age = age;
1420            if age.0 < 18 {
1421                AgeGroup::Child
1422            } else if age.0 < 65 {
1423                AgeGroup::Adult
1424            } else {
1425                AgeGroup::Senior
1426            }
1427        }
1428    );
1429
1430    // Derived property - computed from other properties
1431    define_derived_property!(struct DerivedProp(bool), Person, [Age],
1432        |age| {
1433            DerivedProp(age.0 % 2 == 0)
1434        }
1435    );
1436
1437    define_derived_property!(
1438        struct DerivedMaybeAge(Option<u8>),
1439        Person,
1440        [Age],
1441        |age| DerivedMaybeAge((age.0 != 0).then_some(age.0))
1442    );
1443
1444    define_derived_property!(
1445        struct DerivedMaybeWeight(Option<f64>),
1446        Person,
1447        [Age],
1448        |age| DerivedMaybeWeight((age.0 != 0).then_some(age.0 as f64)),
1449        impl_eq_hash = both
1450    );
1451
1452    define_derived_property!(
1453        struct DerivedMaybeAgeCustom(Option<u8>),
1454        Person,
1455        [Age],
1456        |age| DerivedMaybeAgeCustom((age.0 != 0).then_some(age.0)),
1457        display_impl = |value: &DerivedMaybeAgeCustom| match value.0 {
1458            Some(v) => format!("derived:{v}"),
1459            None => "derived:none".to_string(),
1460        }
1461    );
1462
1463    define_derived_property!(
1464        struct DerivedWeight(f64),
1465        Person,
1466        [Age],
1467        |age| DerivedWeight(age.0 as f64),
1468        impl_eq_hash = both
1469    );
1470
1471    #[derive(Debug, PartialEq, Clone, Copy)]
1472    struct NonIndexableFloat(f64);
1473    impl_property!(
1474        NonIndexableFloat,
1475        Person,
1476        default_const = NonIndexableFloat(0.0)
1477    );
1478
1479    // A property type for two distinct entities.
1480    #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
1481    pub enum InfectionKind {
1482        Respiratory,
1483        Genetic,
1484        Superficial,
1485    }
1486    impl_property!(
1487        InfectionKind,
1488        Person,
1489        default_const = InfectionKind::Respiratory
1490    );
1491    impl_property!(InfectionKind, Group, default_const = InfectionKind::Genetic);
1492
1493    define_multi_property!(Person, (Name, Age, Weight));
1494    define_multi_property!(Person, (Age, Weight, Name));
1495    define_multi_property!(Person, (Weight, Age, Name));
1496    define_multi_property!(Person, (Name, Weight,),);
1497
1498    // For convenience
1499    type ProfileNAW = (Name, Age, Weight);
1500    type ProfileAWN = (Age, Weight, Name);
1501    type ProfileWAN = (Weight, Age, Name);
1502    type ProfileNW = (Name, Weight);
1503
1504    define_entity!(SingleProfilePerson);
1505    define_property!(
1506        struct SingleName(&'static str),
1507        SingleProfilePerson,
1508        default_const = SingleName("")
1509    );
1510    define_property!(
1511        struct SingleAge(u8),
1512        SingleProfilePerson,
1513        default_const = SingleAge(0)
1514    );
1515    define_property!(
1516        struct SingleWeight(u8),
1517        SingleProfilePerson,
1518        default_const = SingleWeight(0)
1519    );
1520    define_multi_property!(SingleProfilePerson, (SingleName, SingleAge, SingleWeight));
1521    type SingleProfile = (SingleName, SingleAge, SingleWeight);
1522
1523    #[test]
1524    fn test_multi_property_ordering() {
1525        let a = (Name("Jane"), Age(22), Weight(180.5));
1526        let b = (Age(22), Weight(180.5), Name("Jane"));
1527        let c = (Weight(180.5), Age(22), Name("Jane"));
1528
1529        // Equivalent multi-properties keep distinct storage and type identities.
1530        // Query routing equivalence is handled by the multi-property registry.
1531        assert_ne!(ProfileNAW::id(), ProfileAWN::id());
1532        assert_ne!(ProfileNAW::id(), ProfileWAN::id());
1533        assert_ne!(ProfileNAW::type_id(), ProfileAWN::type_id());
1534        assert_ne!(ProfileNAW::type_id(), ProfileWAN::type_id());
1535        let _ = ProfileNW::id();
1536
1537        let query_parts = ProfileNAW::query_parts_for_value(&a);
1538        assert_eq!(
1539            ProfileAWN::value_from_query_parts(query_parts.as_ref()),
1540            Some(b)
1541        );
1542        assert_eq!(
1543            ProfileWAN::value_from_query_parts(query_parts.as_ref()),
1544            Some(c)
1545        );
1546    }
1547
1548    #[test]
1549    fn test_non_indexable_property_unindexed_behavior() {
1550        let mut context = Context::new();
1551
1552        let first = context
1553            .add_entity(with!(Person, NonIndexableFloat(1.5)))
1554            .unwrap();
1555        let _second = context
1556            .add_entity(with!(Person, NonIndexableFloat(2.5)))
1557            .unwrap();
1558
1559        assert_eq!(
1560            context.get_property::<Person, NonIndexableFloat>(first),
1561            NonIndexableFloat(1.5)
1562        );
1563
1564        context.set_property(first, NonIndexableFloat(3.5));
1565        assert_eq!(
1566            context.get_property::<Person, NonIndexableFloat>(first),
1567            NonIndexableFloat(3.5)
1568        );
1569
1570        let mut results = Vec::new();
1571        context.with_query_results(with!(Person, NonIndexableFloat(3.5)), &mut |entity_ids| {
1572            results = entity_ids.into_iter().collect::<Vec<_>>();
1573        });
1574        assert_eq!(results, vec![first]);
1575        assert_eq!(
1576            context.query_entity_count(with!(Person, NonIndexableFloat(3.5))),
1577            1
1578        );
1579    }
1580
1581    #[test]
1582    fn test_single_multi_property_vs_property_query() {
1583        let mut context = Context::new();
1584
1585        context
1586            .add_entity(with!(
1587                SingleProfilePerson,
1588                SingleName("John"),
1589                SingleAge(42),
1590                SingleWeight(220)
1591            ))
1592            .unwrap();
1593        context
1594            .add_entity(with!(
1595                SingleProfilePerson,
1596                SingleName("Jane"),
1597                SingleAge(22),
1598                SingleWeight(180)
1599            ))
1600            .unwrap();
1601        context
1602            .add_entity(with!(
1603                SingleProfilePerson,
1604                SingleName("Bob"),
1605                SingleAge(32),
1606                SingleWeight(190)
1607            ))
1608            .unwrap();
1609        context
1610            .add_entity(with!(
1611                SingleProfilePerson,
1612                SingleName("Alice"),
1613                SingleAge(22),
1614                SingleWeight(170)
1615            ))
1616            .unwrap();
1617
1618        context.index_property::<SingleProfilePerson, SingleProfile>();
1619
1620        let example_query = (SingleName("Alice"), SingleAge(22), SingleWeight(170));
1621        assert_eq!(
1622            <SingleProfile as QueryInternal<SingleProfilePerson>>::multi_property_id(
1623                &example_query
1624            ),
1625            Some(SingleProfile::id())
1626        );
1627        let query_parts = QueryInternal::query_parts(&example_query);
1628        assert_eq!(
1629            SingleProfile::value_from_query_parts(query_parts.as_ref()),
1630            Some((SingleName("Alice"), SingleAge(22), SingleWeight(170)))
1631        );
1632
1633        context.with_query_results(
1634            with!(
1635                SingleProfilePerson,
1636                (SingleName("John"), SingleAge(42), SingleWeight(220))
1637            ),
1638            &mut |results| {
1639                assert_eq!(results.into_iter().count(), 1);
1640            },
1641        );
1642    }
1643
1644    #[test]
1645    fn test_equivalent_multi_property_query_routing() {
1646        let example_query = (Name("Alice"), Age(22), Weight(170.5));
1647        let query_multi_property_id =
1648            <(Name, Age, Weight) as QueryInternal<Person>>::multi_property_id(&example_query);
1649
1650        assert!(matches!(
1651            query_multi_property_id,
1652            Some(id) if id == ProfileNAW::id() || id == ProfileAWN::id() || id == ProfileWAN::id()
1653        ));
1654
1655        let query_parts = QueryInternal::query_parts(&example_query);
1656        assert_eq!(
1657            ProfileNAW::value_from_query_parts(query_parts.as_ref()),
1658            Some((Name("Alice"), Age(22), Weight(170.5)))
1659        );
1660        assert_eq!(
1661            ProfileAWN::value_from_query_parts(query_parts.as_ref()),
1662            Some((Age(22), Weight(170.5), Name("Alice")))
1663        );
1664        assert_eq!(
1665            ProfileWAN::value_from_query_parts(query_parts.as_ref()),
1666            Some((Weight(170.5), Age(22), Name("Alice")))
1667        );
1668    }
1669
1670    #[test]
1671    fn test_derived_property() {
1672        let mut context = Context::new();
1673
1674        let senior = context
1675            .add_entity::<Person, _>(with!(Person, Age(92)))
1676            .unwrap();
1677        let child = context
1678            .add_entity::<Person, _>(with!(Person, Age(12)))
1679            .unwrap();
1680        let adult = context
1681            .add_entity::<Person, _>(with!(Person, Age(44)))
1682            .unwrap();
1683
1684        let senior_group: AgeGroup = context.get_property(senior);
1685        let child_group: AgeGroup = context.get_property(child);
1686        let adult_group: AgeGroup = context.get_property(adult);
1687
1688        assert_eq!(senior_group, AgeGroup::Senior);
1689        assert_eq!(child_group, AgeGroup::Child);
1690        assert_eq!(adult_group, AgeGroup::Adult);
1691
1692        // Age has no dependencies (only dependents)
1693        assert!(Age::non_derived_dependencies().is_empty());
1694        // AgeGroup depends only on Age
1695        assert_eq!(AgeGroup::non_derived_dependencies(), [Age::id()]);
1696
1697        // Age has several dependents. This assert may break if you add or remove the properties in this test module.
1698        let mut expected_dependents = [
1699            AgeGroup::id(),
1700            DerivedProp::id(),
1701            DerivedMaybeAge::id(),
1702            DerivedMaybeWeight::id(),
1703            DerivedMaybeAgeCustom::id(),
1704            DerivedWeight::id(),
1705            ProfileNAW::id(),
1706            ProfileAWN::id(),
1707            ProfileWAN::id(),
1708        ];
1709        expected_dependents.sort_unstable();
1710        assert_eq!(Age::dependents(), expected_dependents);
1711    }
1712
1713    #[test]
1714    fn test_get_display() {
1715        let mut context = Context::new();
1716        let person = context
1717            .add_entity(with!(Person, POu32(Some(42)), Pu32(22)))
1718            .unwrap();
1719        assert_eq!(
1720            POu32::get_display(&context.get_property::<_, POu32>(person)).to_string(),
1721            "42"
1722        );
1723        assert_eq!(
1724            Pu32::get_display(&context.get_property::<_, Pu32>(person)).to_string(),
1725            "Pu32(22)"
1726        );
1727        let person2 = context
1728            .add_entity(with!(Person, POu32(None), Pu32(11)))
1729            .unwrap();
1730        assert_eq!(
1731            POu32::get_display(&context.get_property::<_, POu32>(person2)).to_string(),
1732            "None"
1733        );
1734    }
1735
1736    #[test]
1737    fn test_option_property_display_patterns() {
1738        let mut context = Context::new();
1739
1740        let some_person = context
1741            .add_entity(with!(
1742                Person,
1743                POu32(Some(42)),
1744                POFloat(Some(3.5)),
1745                POu32Custom(Some(7)),
1746                Pu32(1),
1747            ))
1748            .unwrap();
1749        let none_person = context
1750            .add_entity(with!(
1751                Person,
1752                POu32(None),
1753                POFloat(None),
1754                POu32Custom(None),
1755                Pu32(2)
1756            ))
1757            .unwrap();
1758
1759        assert_eq!(
1760            POu32::get_display(&context.get_property::<_, POu32>(some_person)),
1761            "42"
1762        );
1763        assert_eq!(
1764            POu32::get_display(&context.get_property::<_, POu32>(none_person)),
1765            "None"
1766        );
1767
1768        assert_eq!(
1769            POFloat::get_display(&context.get_property::<_, POFloat>(some_person)),
1770            "3.5"
1771        );
1772        assert_eq!(
1773            POFloat::get_display(&context.get_property::<_, POFloat>(none_person)),
1774            "None"
1775        );
1776
1777        assert_eq!(
1778            POu32Custom::get_display(&context.get_property::<_, POu32Custom>(some_person)),
1779            "custom:7"
1780        );
1781        assert_eq!(
1782            POu32Custom::get_display(&context.get_property::<_, POu32Custom>(none_person)),
1783            "custom:none"
1784        );
1785    }
1786
1787    #[test]
1788    fn test_option_derived_property_display_patterns() {
1789        let mut context = Context::new();
1790
1791        let some_person = context
1792            .add_entity::<Person, _>(with!(Person, Age(42)))
1793            .unwrap();
1794        let none_person = context
1795            .add_entity::<Person, _>(with!(Person, Age(0)))
1796            .unwrap();
1797
1798        assert_eq!(
1799            DerivedMaybeAge::get_display(&context.get_property::<_, DerivedMaybeAge>(some_person)),
1800            "42"
1801        );
1802        assert_eq!(
1803            DerivedMaybeAge::get_display(&context.get_property::<_, DerivedMaybeAge>(none_person)),
1804            "None"
1805        );
1806
1807        assert_eq!(
1808            DerivedMaybeWeight::get_display(
1809                &context.get_property::<_, DerivedMaybeWeight>(some_person)
1810            ),
1811            "42.0"
1812        );
1813        assert_eq!(
1814            DerivedMaybeWeight::get_display(
1815                &context.get_property::<_, DerivedMaybeWeight>(none_person)
1816            ),
1817            "None"
1818        );
1819
1820        assert_eq!(
1821            DerivedMaybeAgeCustom::get_display(
1822                &context.get_property::<_, DerivedMaybeAgeCustom>(some_person)
1823            ),
1824            "derived:42"
1825        );
1826        assert_eq!(
1827            DerivedMaybeAgeCustom::get_display(
1828                &context.get_property::<_, DerivedMaybeAgeCustom>(none_person)
1829            ),
1830            "derived:none"
1831        );
1832    }
1833
1834    #[test]
1835    fn test_debug_trait() {
1836        let property = Pu32(11);
1837        let debug_str = format!("{:?}", property);
1838        assert_eq!(debug_str, "Pu32(11)");
1839
1840        let property = POu32(Some(22));
1841        let debug_str = format!("{:?}", property);
1842        assert_eq!(debug_str, "POu32(Some(22))");
1843    }
1844
1845    #[test]
1846    fn test_define_derived_property_impl_eq_hash() {
1847        let mut values = crate::HashSet::default();
1848        values.insert(DerivedWeight(3.0));
1849        assert!(values.contains(&DerivedWeight(3.0)));
1850    }
1851}