specs_task/
cons.rs

1use crate::{components::*, user::TaskUser};
2
3use specs::prelude::*;
4
5/// Implemented by all nodes of a `TaskGraph`. Has a blanket impl that should work for most
6/// `TaskComponent`s.
7pub trait TaskFactory {
8    fn create_task(&self, user: &TaskUser) -> Entity;
9}
10
11impl<'a, T: 'static + Clone + TaskComponent<'a> + Send + Sync> TaskFactory for T {
12    fn create_task(&self, user: &TaskUser) -> Entity {
13        user.make_task_lazy(self.clone())
14    }
15}
16
17impl TaskFactory for Entity {
18    fn create_task(&self, _user: &TaskUser) -> Entity {
19        self.clone()
20    }
21}
22
23// PERF: Cons requires a lot of heap allocations, but this choice was made to avoid using recursive
24// types which prevent assigning different graphs to a single variable (e.g. accumulating a graph in
25// a loop).
26
27/// Implementation detail of `TaskGraph`. Note that two trees may be unequal yet equivalent in how
28/// they `assemble`, for example, `Cons::Seq(x, Cons::Seq(y, z)) != Cons::Seq(Cons::Seq(x, y), z)`,
29/// but they both assemble into a sequence `x -> y -> z`.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum Cons<T> {
32    Fork(Box<Cons<T>>, Box<Cons<T>>),
33    Seq(Box<Cons<T>>, Box<Cons<T>>),
34    Task(T),
35    Nil, // currently required to support graph accumulation
36}
37
38impl<T> Cons<T> {
39    fn remove_nil(self) -> Self {
40        match self {
41            Cons::Seq(head, tail) => match (*head, *tail) {
42                (Cons::Nil, Cons::Nil) => Cons::Nil,
43                (Cons::Nil, t) => t.remove_nil(),
44                (h, Cons::Nil) => h.remove_nil(),
45                (h, t) => Cons::Seq(Box::new(h.remove_nil()), Box::new(t.remove_nil())),
46            },
47            Cons::Fork(head, tail) => match (*head, *tail) {
48                (Cons::Nil, Cons::Nil) => Cons::Nil,
49                (Cons::Nil, t) => t.remove_nil(),
50                (h, Cons::Nil) => h.remove_nil(),
51                (h, t) => Cons::Fork(Box::new(h.remove_nil()), Box::new(t.remove_nil())),
52            },
53            Cons::Task(t) => Cons::Task(t),
54            Cons::Nil => Cons::Nil,
55        }
56    }
57}
58
59/// A node of the binary tree grammar that describes a task graph. `Cons::Seq` lists represent
60/// sequential execution of tasks. `Cons::Fork` lists represent concurrent execution of tasks. The
61/// leaves of the tree are `Cons::Task`s.
62pub type TaskGraph = Cons<Box<dyn TaskFactory>>;
63
64impl TaskGraph {
65    fn _assemble(self, fork: Option<Entity>, user: &TaskUser) -> (Entity, Entity) {
66        match self {
67            Cons::Seq(head, tail) => {
68                let (head_first_entity, head_last_entity) = head._assemble(None, user);
69                let (tail_first_entity, tail_last_entity) = tail._assemble(None, user);
70                user.join_lazy(tail_first_entity, head_last_entity);
71
72                (head_first_entity, tail_last_entity)
73            }
74            Cons::Fork(head, tail) => {
75                let fork_entity = if let Some(e) = fork {
76                    e
77                } else {
78                    user.make_fork_lazy()
79                };
80
81                let (_, head_last_entity) = head._assemble(Some(fork_entity), user);
82                let (_, tail_last_entity) = tail._assemble(Some(fork_entity), user);
83
84                // Any decendents reachable only via Cons::Fork are considered prongs. If a
85                // descendent is a Cons::Seq, then the prong only connects at the "last" entity of
86                // the sequence.
87                if head_last_entity != fork_entity {
88                    user.add_prong_lazy(fork_entity, head_last_entity);
89                }
90                if tail_last_entity != fork_entity {
91                    user.add_prong_lazy(fork_entity, tail_last_entity);
92                }
93
94                (fork_entity, fork_entity)
95            }
96            Cons::Task(task) => {
97                let task_entity = task.create_task(user);
98
99                (task_entity, task_entity)
100            }
101            Cons::Nil => panic!("Tried to assemble Cons::Nil, which should always be removed."),
102        }
103    }
104
105    /// Lazily create all of the task graph entities and connect them with edges. Tasks won't exist
106    /// until calling `World::maintain`.
107    pub fn assemble(self, on_completion: OnCompletion, user: &TaskUser) -> Option<Entity> {
108        let s = self.remove_nil();
109        if let Cons::Nil = s {
110            return None;
111        }
112
113        let (_first_entity, last_entity) = s._assemble(None, user);
114        user.finalize_lazy(last_entity, on_completion);
115
116        Some(last_entity)
117    }
118}
119
120// TODO: Get rid of the "@" that precedes every task expression. I am bad at macros, please help!
121
122/// Make a task graph without any tasks. This is used as the initial value for accumulating graphs
123/// dynamically.
124#[macro_export]
125macro_rules! empty_graph {
126    () => {
127        Cons::Nil
128    };
129}
130
131/// Make a single-node `TaskGraph`.
132#[macro_export]
133macro_rules! task {
134    (@$task:expr) => {
135        Cons::Task(Box::new($task))
136    };
137}
138
139// TODO: deduplicate these definitions that are mostly the same
140
141/// Returns a `TaskGraph` that executes the argument list of `TaskGraphs` concurrently.
142#[macro_export]
143macro_rules! fork {
144    (@$head:expr, $($tail:tt)*) => (
145        Cons::Fork(Box::new(fork!(@$head)), Box::new(fork!($($tail)*)))
146    );
147    ($head:expr, $($tail:tt)*) => (
148        Cons::Fork(Box::new(fork!($head)), Box::new(fork!($($tail)*)))
149    );
150    (@$task:expr) => (
151        Cons::Task(Box::new($task))
152    );
153    ($head:expr) => ( $head );
154}
155
156/// Returns a `TaskGraph` that executes the argument list of `TaskGraphs` sequentially.
157#[macro_export]
158macro_rules! seq {
159    (@$head:expr, $($tail:tt)*) => (
160        Cons::Seq(Box::new(seq!(@$head)), Box::new(seq!($($tail)*)))
161    );
162    ($head:expr, $($tail:tt)*) => (
163        Cons::Seq(Box::new(seq!($head)), Box::new(seq!($($tail)*)))
164    );
165    (@$task:expr) => (
166        Cons::Task(Box::new($task))
167    );
168    ($head:expr) => ( $head );
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[derive(Clone, Debug, Eq, PartialEq)]
176    struct Foo(u32);
177
178    #[test]
179    fn task_graph_macro_fork_two() {
180        let x = fork!(@Foo(1u32), @Foo(2u32));
181        assert_eq!(
182            x,
183            Cons::Fork(
184                Box::new(Cons::Task(Box::new(Foo(1u32)))),
185                Box::new(Cons::Task(Box::new(Foo(2u32)))),
186            )
187        );
188    }
189
190    #[test]
191    fn task_graph_macro_fork_three() {
192        let x = fork!(@Foo(1u32), @Foo(2u32), @Foo(3u32));
193        assert_eq!(
194            x,
195            Cons::Fork(
196                Box::new(Cons::Task(Box::new(Foo(1u32)))),
197                Box::new(Cons::Fork(
198                    Box::new(Cons::Task(Box::new(Foo(2u32)))),
199                    Box::new(Cons::Task(Box::new(Foo(3u32)))),
200                )),
201            )
202        );
203    }
204
205    #[test]
206    fn task_graph_macro_nested_fork() {
207        let x: Cons<Box<Foo>> = fork!(@Foo(1u32), fork!(@Foo(2u32), @Foo(3u32)));
208        assert_eq!(
209            x,
210            Cons::Fork(
211                Box::new(Cons::Task(Box::new(Foo(1u32)))),
212                Box::new(Cons::Fork(
213                    Box::new(Cons::Task(Box::new(Foo(2u32)))),
214                    Box::new(Cons::Task(Box::new(Foo(3u32)))),
215                )),
216            )
217        );
218    }
219
220    #[test]
221    fn task_graph_macro_many_nested() {
222        let x = seq!(
223            @Foo(1u32),
224            fork!(
225                seq!(@Foo(2u32), @Foo(3u32), @Foo(4u32)),
226                @Foo(5u32),
227                @Foo(6u32)
228            ),
229            @Foo(7u32)
230        );
231        let y: Cons<Box<Foo>> = Cons::Seq(
232            Box::new(Cons::Task(Box::new(Foo(1u32)))),
233            Box::new(Cons::Seq(
234                Box::new(Cons::Fork(
235                    Box::new(Cons::Seq(
236                        Box::new(Cons::Task(Box::new(Foo(2u32)))),
237                        Box::new(Cons::Seq(
238                            Box::new(Cons::Task(Box::new(Foo(3u32)))),
239                            Box::new(Cons::Task(Box::new(Foo(4u32)))),
240                        )),
241                    )),
242                    Box::new(Cons::Fork(
243                        Box::new(Cons::Task(Box::new(Foo(5u32)))),
244                        Box::new(Cons::Task(Box::new(Foo(6u32)))),
245                    )),
246                )),
247                Box::new(Cons::Task(Box::new(Foo(7u32)))),
248            )),
249        );
250        assert_eq!(x, y);
251    }
252
253    #[test]
254    fn remove_nil_from_left_fork() {
255        let x = fork!(Cons::Nil, @Foo(1));
256        assert_eq!(x.remove_nil(), task!(@Foo(1)));
257    }
258
259    #[test]
260    fn remove_nil_from_right_fork() {
261        let x = fork!(@Foo(1), Cons::Nil);
262        assert_eq!(x.remove_nil(), task!(@Foo(1)));
263    }
264
265    #[test]
266    fn remove_all_nils_nested_fork() {
267        let x = fork!(Cons::Nil, fork!(Cons::Nil, @Foo(1)));
268        assert_eq!(x.remove_nil(), task!(@Foo(1)));
269    }
270
271    #[test]
272    fn accumulate_sequence_in_loop() {
273        let mut s = empty_graph!();
274        for i in 0..4 {
275            s = seq!(s, @Foo(i));
276        }
277        // Unfortunately removing nils puts the tree in an equivalent but not equal shape.
278        assert_eq!(
279            s.remove_nil(),
280            seq!(seq!(seq!(@Foo(0), @Foo(1)), @Foo(2)), @Foo(3))
281        );
282    }
283}