Skip to main content

dope_fiber/task/batch/
join.rs

1use core::{convert, pin, task::Poll};
2
3use o3::depot::array;
4
5use crate::{
6    abi,
7    abi::machine,
8    context,
9    context::wake,
10    core::driver::schedule::{
11        app,
12        ready::{
13            task,
14            task::{pool, topology},
15        },
16    },
17    task::{
18        batch::{Completed, Error, Index, Pool, RootError, Tag, state},
19        storage::slots,
20    },
21};
22
23const LIMIT: usize = 32;
24
25enum Progress<const N: usize> {
26    Done(Completed<N>),
27    More,
28    Parked,
29}
30
31/// One ordered, fixed-capacity group of homogeneous fibers.
32#[pin_project::pin_project(PinnedDrop)]
33#[must_use = "a fiber does nothing unless it is driven"]
34pub struct Join<'pool, 'd, F, const N: usize>
35where
36    F: abi::Fiber<'d>,
37{
38    #[pin]
39    table: slots::Table<'d, F, F::Output, N>,
40    pool: &'pool Pool<'d, N>,
41    state: state::State<N>,
42    live: machine::Machine<()>,
43    root: Option<pool::Root<'d>>,
44}
45
46/// Exclusive proof of the next empty entry in one batch.
47#[must_use = "a batch vacancy must be filled or abandoned"]
48struct Vacant<'batch, F, O, const N: usize> {
49    entry: slots::raw::Vacant<'batch, F, O>,
50    state: state::Push<'batch, N>,
51}
52
53impl<F, O, const N: usize> Vacant<'_, F, O, N> {
54    fn insert(self, fiber: F) {
55        self.entry.insert(fiber);
56        self.state.commit();
57    }
58}
59
60impl<'pool, 'd, F, const N: usize> Join<'pool, 'd, F, N>
61where
62    F: abi::Fiber<'d>,
63{
64    /// Builds an empty fixed-capacity batch.
65    pub fn new(pool: &'pool mut Pool<'d, N>) -> Self {
66        Self::from_table(pool, slots::Table::vacant(), array::Len::ZERO)
67    }
68
69    /// Builds a batch containing every fiber in array order.
70    pub fn from_array(pool: &'pool mut Pool<'d, N>, fibers: [F; N]) -> Self {
71        Self::from_map(pool, fibers, convert::identity)
72    }
73
74    /// Consumes each input in array order, constructing every fiber before returning.
75    pub fn from_map<P>(
76        pool: &'pool mut Pool<'d, N>,
77        input: [P; N],
78        make: impl FnMut(P) -> F,
79    ) -> Self {
80        Self::from_table(pool, slots::Table::from_map(input, make), array::Len::MAX)
81    }
82
83    /// Builds a full batch without materializing an intermediate fiber array.
84    pub fn from_fn(pool: &'pool mut Pool<'d, N>, make: impl FnMut(Index<N>) -> F) -> Self {
85        Self::from_table(pool, slots::Table::from_fn(make), array::Len::MAX)
86    }
87
88    fn from_table(
89        pool: &'pool mut Pool<'d, N>,
90        table: slots::Table<'d, F, F::Output, N>,
91        len: array::Len<N>,
92    ) -> Self {
93        Self {
94            table,
95            pool,
96            state: state::State::new(len),
97            live: machine::Machine::new(()),
98            root: None,
99        }
100    }
101
102    /// Borrows the next entry while the batch is still being built.
103    fn vacant(&mut self) -> Option<Vacant<'_, F, F::Output, N>> {
104        let state = self.state.vacancy()?;
105        let index = slots::Index::from_vacant(state.slot());
106        // SAFETY: only this owner pairs its table and admitted prefix. Empty
107        // construction initializes every entry as vacant; full construction has
108        // no vacancy. Each insertion fills exactly the next entry and commits
109        // that same prefix. This exclusive borrow cannot overlap a pinned poll,
110        // cancellation, or another insertion.
111        let entry = unsafe { slots::raw::Vacant::new(&mut self.table, index) };
112        Some(Vacant { entry, state })
113    }
114
115    pub fn try_push(&mut self, fiber: F) -> Result<(), F> {
116        let Some(vacant) = self.vacant() else {
117            return Err(fiber);
118        };
119        vacant.insert(fiber);
120        Ok(())
121    }
122
123    fn progress(
124        self: pin::Pin<&mut Self>,
125        mut context: pin::Pin<&mut context::Context<'_, 'd>>,
126    ) -> Result<Progress<N>, Error> {
127        let mut this = self.project();
128        if let Some(done) = this.state.completed() {
129            return Ok(
130                match this
131                    .live
132                    .turn(|()| machine::Flow::Suspend(Poll::Ready(done)))
133                {
134                    Poll::Ready(done) => Progress::Done(done),
135                    Poll::Pending => Progress::Parked,
136                },
137            );
138        }
139
140        let parent: wake::Waker<'d> = context.as_ref().root_waker().into();
141        let pool = *this.pool;
142        let root = match pool.tasks.try_root(parent.0) {
143            Ok(root) => root,
144            Err(pool::RootError::Foreign) => {
145                return Err(Error::Root(RootError::Foreign));
146            }
147            Err(pool::RootError::Invalid(error)) => {
148                return Err(Error::Root(RootError::Invalid(error)));
149            }
150        };
151        match *this.root {
152            None => *this.root = Some(root),
153            Some(active) if active == root => {}
154            Some(active) => {
155                return Err(Error::Root(RootError::Changed {
156                    active: active.id(),
157                    observed: root.id(),
158                }));
159            }
160        }
161
162        let mut budget = LIMIT;
163        let mut initial = true;
164        while budget != 0 {
165            let Some(fresh) = this.state.fresh() else {
166                break;
167            };
168            let index = slots::Index::from_vacant(fresh.slot());
169            let permit = if initial {
170                initial = false;
171                None
172            } else {
173                match context.as_ref().try_continue() {
174                    context::Continue::Permit(permit) => Some(permit),
175                    context::Continue::Exhausted => return Ok(Progress::More),
176                }
177            };
178            let attach = pool.ready.attach(index);
179            let done = poll_slot(
180                this.table.as_mut(),
181                context.as_mut(),
182                index,
183                attach,
184                permit,
185                &pool.tasks,
186                root,
187            )
188            .map_err(Error::Task)?;
189            budget -= 1;
190            if let state::Status::Done(all) = fresh.commit(done) {
191                return Ok(Progress::Done(all));
192            }
193        }
194
195        if budget == 0 {
196            return Ok(if this.state.has_fresh() || !pool.ready.is_empty() {
197                Progress::More
198            } else {
199                Progress::Parked
200            });
201        }
202
203        let Some(mut drain) = pool.ready.drain() else {
204            return Ok(Progress::Parked);
205        };
206        loop {
207            let pending = match this.state.pending() {
208                state::PendingState::Live(pending) => pending,
209                state::PendingState::Done(done) => return Ok(Progress::Done(done)),
210            };
211            let (index, permit) = if initial {
212                let Some(index) = drain.pop() else {
213                    return Ok(Progress::Parked);
214                };
215                initial = false;
216                (index, None)
217            } else {
218                match context.as_ref().work().next(&mut drain) {
219                    app::Next::Item(index, permit) => (index, Some(permit)),
220                    app::Next::Empty => return Ok(Progress::Parked),
221                    app::Next::Exhausted(_) => return Ok(Progress::More),
222                }
223            };
224            let attach = pool.ready.attach(index);
225            budget -= 1;
226            let done = poll_slot(
227                this.table.as_mut(),
228                context.as_mut(),
229                index,
230                attach,
231                permit,
232                &pool.tasks,
233                root,
234            )
235            .map_err(Error::Task)?;
236            if let state::Status::Done(all) = pending.commit(done) {
237                return Ok(Progress::Done(all));
238            }
239            if budget == 0 {
240                return Ok(if drain.peek().is_some() {
241                    Progress::More
242                } else {
243                    Progress::Parked
244                });
245            }
246        }
247    }
248
249    fn cancel(self: pin::Pin<&mut Self>) {
250        let mut this = self.project();
251        if let Some(root) = *this.root {
252            let pool = *this.pool;
253            for index in slots::Index::until(this.state.next()) {
254                this.table.as_mut().cancel(index, |bindings| {
255                    bindings.reclaim_pool(index, &pool.tasks, root);
256                });
257            }
258        }
259        this.state.reset();
260    }
261}
262
263fn poll_slot<'turn, 'd: 'turn, F, const N: usize>(
264    mut table: pin::Pin<&mut slots::Table<'d, F, F::Output, N>>,
265    mut context: pin::Pin<&mut context::Context<'turn, 'd>>,
266    index: slots::Index<N>,
267    edge: topology::Edge<'_, 'd, slots::Index<N>>,
268    permit: Option<app::Permit<'turn, 'd>>,
269    pool: &pool::Pool<'d, Tag, N>,
270    root: pool::Root<'d>,
271) -> Result<state::Completion, task::Error>
272where
273    F: abi::Fiber<'d>,
274{
275    match table.as_mut().visit(index, (), |fiber, bindings, ()| {
276        let poll = bindings.poll_pool(edge, pool, root, |wake| {
277            match permit {
278                Some(permit) => unsafe {
279                    context::raw::Live::new(fiber).child_with(context.as_mut(), wake, permit)
280                },
281                None => {
282                    // SAFETY: progress consumes its original PollCall admission
283                    // for exactly the first live child; later children carry permits.
284                    // Table::visit ends this child's live state on Ready.
285                    unsafe { context::raw::Live::new(fiber).child(context.as_mut(), wake) }
286                }
287            }
288        });
289        let poll = match poll {
290            Ok(poll) => poll,
291            Err(error) => return slots::Action::Keep(Err(error)),
292        };
293        let Poll::Ready(output) = poll else {
294            return slots::Action::Keep(Ok(state::Completion::Pending));
295        };
296        slots::Action::Complete(output, Ok(state::Completion::Ready))
297    }) {
298        slots::Phase::Live(result) => result,
299        slots::Phase::Vacant(()) | slots::Phase::Done(()) => Ok(state::Completion::Pending),
300    }
301}
302
303impl<'pool, 'd, F, const N: usize> abi::Fiber<'d> for Join<'pool, 'd, F, N>
304where
305    F: abi::Fiber<'d>,
306{
307    type Output = Result<array::IntoIter<F::Output, N>, Error>;
308
309    fn poll(call: context::PollCall<'_, '_, 'd, Self>) -> Poll<Self::Output> {
310        let (mut this, mut context) = call.into_parts();
311        match this.as_mut().progress(context.as_mut()) {
312            Ok(Progress::Done(done)) => {
313                let output = this.as_mut().project().table.drain_exact(done);
314                let _ = this.as_mut().project().live.take();
315                Poll::Ready(Ok(output))
316            }
317            Ok(Progress::Parked) => Poll::Pending,
318            Ok(Progress::More) => {
319                context.requeue();
320                Poll::Pending
321            }
322            Err(error) => {
323                this.as_mut().cancel();
324                let _ = this.as_mut().project().live.take();
325                Poll::Ready(Err(error))
326            }
327        }
328    }
329}
330
331#[pin_project::pinned_drop]
332impl<'d, F, const N: usize> PinnedDrop for Join<'_, 'd, F, N>
333where
334    F: abi::Fiber<'d>,
335{
336    fn drop(mut self: pin::Pin<&mut Self>) {
337        if self.as_mut().project().live.take().is_some() {
338            self.cancel();
339        }
340    }
341}
342
343const _: () = {
344    use core::mem;
345
346    let word = mem::size_of::<usize>();
347    assert!(mem::size_of::<machine::Machine<()>>() == 1);
348    assert!(mem::size_of::<Join<'static, 'static, abi::Ready<()>, 0>>() == 4 * word);
349    assert!(mem::align_of::<Join<'static, 'static, abi::Ready<()>, 0>>() == word);
350    assert!(mem::size_of::<Option<Join<'static, 'static, abi::Ready<()>, 0>>>() == 4 * word);
351};