Skip to main content

unlit_ecs/query/
macros.rs

1//! The declarative front end for a query.
2//!
3//! Writing a query by hand takes three pieces: a struct of items, a type for the
4//! plan, and an impl with two bodies. This macro derives all of it from one
5//! declaration, so what the caller writes is the shape of one row:
6//!
7//! ```rust
8//! # use unlit_ecs::{Entity, Has, World, bundle, query};
9//! pub struct Health(pub i32);
10//! pub struct Armor(pub i32);
11//!
12//! query! {
13//!     /// One combatant, as the combat pass reads it.
14//!     pub struct Combatant<'a> {
15//!         pub entity: Entity,
16//!         pub health: &'a mut Health,
17//!         pub armor: &'a Armor,
18//!     }
19//! }
20//!
21//! let mut world = World::new();
22//! let entity = world.root_scope_mut().spawn(bundle![Health(10), Armor(2)]);
23//!
24//! for row in world.root_scope().query::<Combatant>().iter() {
25//!     row.health.0 -= row.armor.0;
26//! }
27//!
28//! assert_eq!(world.root_scope().query_one::<&Health>(entity).unwrap().get().0, 8);
29//! ```
30//!
31//! The lifetime is nominal: the items a query yields borrow from the holder,
32//! never from the declaration, so `Combatant`, `Combatant<'static>` and
33//! `Combatant<'_>` name the same query.
34//!
35//! # Fields
36//!
37//! | Field | Yields | Matches |
38//! | --- | --- | --- |
39//! | `&'a T` | `&'a T` | only archetypes carrying `T` |
40//! | `&'a mut T` | `&'a mut T` | only archetypes carrying `T` |
41//! | `Option<&'a T>` | `Option<&'a T>` | every archetype |
42//! | `Option<&'a mut T>` | `Option<&'a mut T>` | every archetype |
43//! | `Entity` | `Entity` | every archetype |
44//! | `Has<T>` | `bool` | every archetype |
45//!
46//! A field whose type is none of these is reported at compile time, naming the
47//! field. A component named twice with at least one of the two exclusive panics
48//! when the query is iterated, the same as `(&T, &mut T)` does.
49//!
50//! # Visibility
51//!
52//! The struct takes the visibility it is declared with, and its fields may carry
53//! attributes and their own visibility: `#[derive(Debug)]`, doc comments and
54//! `pub(crate)` all travel as written.
55
56/// Declare a query from the shape of one row.
57///
58/// See the [module documentation](mod@crate::query) for the fields it accepts.
59#[macro_export]
60macro_rules! query {
61    (
62        $(#[$meta:meta])*
63        $vis:vis struct $name:ident<$lt:lifetime> {
64            $($body:tt)*
65        }
66    ) => {
67        $crate::__query_fields! {
68            [builder row]
69            [$(#[$meta])*]
70            [$vis $name, $lt]
71            []
72            []
73            []
74            []
75            []
76            [$($body)*]
77        }
78    };
79}
80
81#[doc(hidden)]
82#[macro_export]
83macro_rules! __query_fields {
84    // Every field has been rewritten: emit the item struct and the impl.
85    (
86        [$b:ident $r:ident]
87        [$($meta:tt)*]
88        [$vis:vis $name:ident, $lt:lifetime]
89        [$($plan:tt)*]
90        [$($prep:tt)*]
91        [$($bind:tt)*]
92        [$($field:tt)*]
93        [$($init:tt)*]
94        []
95    ) => {
96        $($meta)*
97        $vis struct $name<$lt> {
98            $($field)*
99        }
100
101        impl<$lt> $crate::Query for $name<$lt> {
102            type Item<'r> = $name<'r>;
103            type Plan<'p> = ($($plan)*);
104
105            fn prepare<'p>(
106                $b: &mut $crate::PlanBuilder<'p>,
107            ) -> ::core::option::Option<Self::Plan<'p>> {
108                ::core::option::Option::Some(($($prep)*))
109            }
110
111            fn read<'r, 'p: 'r>(
112                plan: &'r mut Self::Plan<'p>,
113                $r: usize,
114            ) -> Self::Item<'r> {
115                #[expect(
116                    unused_variables,
117                    reason = "a query whose fields claim nothing still binds their plans"
118                )]
119                let ($($bind)*) = plan;
120                $name { $($init)* }
121            }
122        }
123    };
124
125    // Option<&'a mut T>: an exclusive column that may be absent.
126    (
127        [$b:ident $r:ident]
128        [$($meta:tt)*]
129        [$vis:vis $name:ident, $lt:lifetime]
130        [$($plan:tt)*]
131        [$($prep:tt)*]
132        [$($bind:tt)*]
133        [$($field:tt)*]
134        [$($init:tt)*]
135        [$(#[$fmeta:meta])* $fv:vis $f:ident : Option<&$fl:lifetime mut $t:ty>, $($rest:tt)*]
136    ) => {
137        $crate::__query_fields! {
138            [$b $r]
139            [$($meta)*]
140            [$vis $name, $lt]
141            [$($plan)* ::core::option::Option<$crate::ColMut<'p, $t>>,]
142            [$($prep)* $b.write::<$t>(),]
143            [$($bind)* $f,]
144            [$($field)* $(#[$fmeta])* $fv $f: ::core::option::Option<&$lt mut $t>,]
145            [$($init)* $f: $f.as_mut().map(|column| column.at($r)),]
146            [$($rest)*]
147        }
148    };
149
150    // Option<&'a T>: a shared column that may be absent.
151    (
152        [$b:ident $r:ident]
153        [$($meta:tt)*]
154        [$vis:vis $name:ident, $lt:lifetime]
155        [$($plan:tt)*]
156        [$($prep:tt)*]
157        [$($bind:tt)*]
158        [$($field:tt)*]
159        [$($init:tt)*]
160        [$(#[$fmeta:meta])* $fv:vis $f:ident : Option<&$fl:lifetime $t:ty>, $($rest:tt)*]
161    ) => {
162        $crate::__query_fields! {
163            [$b $r]
164            [$($meta)*]
165            [$vis $name, $lt]
166            [$($plan)* ::core::option::Option<$crate::Col<'p, $t>>,]
167            [$($prep)* $b.read::<$t>(),]
168            [$($bind)* $f,]
169            [$($field)* $(#[$fmeta])* $fv $f: ::core::option::Option<&$lt $t>,]
170            [$($init)* $f: $f.as_ref().map(|column| column.at($r)),]
171            [$($rest)*]
172        }
173    };
174
175    // Has<T>: a yes/no question. Claims nothing, so it never narrows the query.
176    (
177        [$b:ident $r:ident]
178        [$($meta:tt)*]
179        [$vis:vis $name:ident, $lt:lifetime]
180        [$($plan:tt)*]
181        [$($prep:tt)*]
182        [$($bind:tt)*]
183        [$($field:tt)*]
184        [$($init:tt)*]
185        [$(#[$fmeta:meta])* $fv:vis $f:ident : Has<$t:ty>, $($rest:tt)*]
186    ) => {
187        $crate::__query_fields! {
188            [$b $r]
189            [$($meta)*]
190            [$vis $name, $lt]
191            [$($plan)* bool,]
192            [$($prep)* $b.has::<$t>(),]
193            [$($bind)* $f,]
194            [$($field)* $(#[$fmeta])* $fv $f: bool,]
195            [$($init)* $f: *$f,]
196            [$($rest)*]
197        }
198    };
199
200    // Entity: the row's own id.
201    (
202        [$b:ident $r:ident]
203        [$($meta:tt)*]
204        [$vis:vis $name:ident, $lt:lifetime]
205        [$($plan:tt)*]
206        [$($prep:tt)*]
207        [$($bind:tt)*]
208        [$($field:tt)*]
209        [$($init:tt)*]
210        [$(#[$fmeta:meta])* $fv:vis $f:ident : Entity, $($rest:tt)*]
211    ) => {
212        $crate::__query_fields! {
213            [$b $r]
214            [$($meta)*]
215            [$vis $name, $lt]
216            [$($plan)* &'p [$crate::Entity],]
217            [$($prep)* $b.entities(),]
218            [$($bind)* $f,]
219            [$($field)* $(#[$fmeta])* $fv $f: $crate::Entity,]
220            [$($init)* $f: $f[$r],]
221            [$($rest)*]
222        }
223    };
224
225    // &'a mut T: an exclusive column.
226    (
227        [$b:ident $r:ident]
228        [$($meta:tt)*]
229        [$vis:vis $name:ident, $lt:lifetime]
230        [$($plan:tt)*]
231        [$($prep:tt)*]
232        [$($bind:tt)*]
233        [$($field:tt)*]
234        [$($init:tt)*]
235        [$(#[$fmeta:meta])* $fv:vis $f:ident : &$fl:lifetime mut $t:ty, $($rest:tt)*]
236    ) => {
237        $crate::__query_fields! {
238            [$b $r]
239            [$($meta)*]
240            [$vis $name, $lt]
241            [$($plan)* $crate::ColMut<'p, $t>,]
242            [$($prep)* $b.write::<$t>()?,]
243            [$($bind)* $f,]
244            [$($field)* $(#[$fmeta])* $fv $f: &$lt mut $t,]
245            [$($init)* $f: $f.at($r),]
246            [$($rest)*]
247        }
248    };
249
250    // &'a T: a shared column.
251    (
252        [$b:ident $r:ident]
253        [$($meta:tt)*]
254        [$vis:vis $name:ident, $lt:lifetime]
255        [$($plan:tt)*]
256        [$($prep:tt)*]
257        [$($bind:tt)*]
258        [$($field:tt)*]
259        [$($init:tt)*]
260        [$(#[$fmeta:meta])* $fv:vis $f:ident : &$fl:lifetime $t:ty, $($rest:tt)*]
261    ) => {
262        $crate::__query_fields! {
263            [$b $r]
264            [$($meta)*]
265            [$vis $name, $lt]
266            [$($plan)* $crate::Col<'p, $t>,]
267            [$($prep)* $b.read::<$t>()?,]
268            [$($bind)* $f,]
269            [$($field)* $(#[$fmeta])* $fv $f: &$lt $t,]
270            [$($init)* $f: $f.at($r),]
271            [$($rest)*]
272        }
273    };
274
275    // A last field written without a trailing comma.
276    (
277        [$b:ident $r:ident]
278        [$($meta:tt)*]
279        [$vis:vis $name:ident, $lt:lifetime]
280        [$($plan:tt)*]
281        [$($prep:tt)*]
282        [$($bind:tt)*]
283        [$($field:tt)*]
284        [$($init:tt)*]
285        [$(#[$fmeta:meta])* $fv:vis $f:ident : $t:ty]
286    ) => {
287        $crate::__query_fields! {
288            [$b $r]
289            [$($meta)*]
290            [$vis $name, $lt]
291            [$($plan)*]
292            [$($prep)*]
293            [$($bind)*]
294            [$($field)*]
295            [$($init)*]
296            [$(#[$fmeta])* $fv $f : $t ,]
297        }
298    };
299
300    // A field whose type is none of the supported shapes.
301    (
302        [$b:ident $r:ident]
303        [$($meta:tt)*]
304        [$vis:vis $name:ident, $lt:lifetime]
305        [$($plan:tt)*]
306        [$($prep:tt)*]
307        [$($bind:tt)*]
308        [$($field:tt)*]
309        [$($init:tt)*]
310        [$(#[$fmeta:meta])* $fv:vis $f:ident : $t:ty $(, $($rest:tt)*)?]
311    ) => {
312        ::core::compile_error!(::core::concat!(
313            "field `", ::core::stringify!($f), "` has an unsupported type; ",
314            "a query field is `&'a T`, `&'a mut T`, `Option<&'a T>`, ",
315            "`Option<&'a mut T>`, `Has<T>` or `Entity`",
316        ));
317    };
318}