odem_rs_core/continuation/mod.rs
1//! The `continuation` module provides the implementation of a type-erased,
2//! intrusively linked futures abstraction used in the scheduler of the
3//! simulation library.
4//!
5//! Continuations are held by a calendar, allowing them to be scheduled and
6//! managed based on model time.
7//!
8//! The [`Continuation`] type represents a continuation that can be scheduled
9//! and executed within the simulation library. It is designed to handle
10//! type-erased futures and supports branding to tie [token witnesses] to their
11//! runtime-state, enabling statically checked validity of state transitions.
12//!
13//! [token witnesses]: token::State
14
15use core::{
16 any::Any,
17 cell::Cell,
18 fmt,
19 hint::unreachable_unchecked,
20 panic::Location,
21 pin::Pin,
22 ptr::NonNull,
23 task::{Context, Poll},
24};
25use intrusive_collections::{LinkedList, RBTreeLink};
26
27pub use adapter::{Adapter, PointerOps};
28pub use puck::Puck;
29pub use share::{Label, Share};
30
31use crate::{
32 Dispatch, ExitStatus,
33 calendar::{PlanState, Scheduler},
34 config::Config,
35 error::NotIdle,
36 fsm::*,
37 ptr::{IntrusivelyCounted, Irc, IrcBox, IrcBoxed},
38 simulator::{Mark, Prec},
39};
40
41mod adapter;
42mod puck;
43mod share;
44
45/* ********************************************************************* Continuation */
46
47/// The type of intrusive link stored in every [Continuation].
48pub type Link = RBTreeLink;
49
50/// A structure of type-erased, intrusively linked [futures].
51///
52/// Pointers to instances of this type are used in the scheduler of the
53/// simulation library. They are oblivious to lifetime-restrictions and
54/// erase the concrete types of the future and the process instance.
55/// The latter types are recovered during dynamic lookup, but the
56/// lifetime-restrictions have to be observed by the abstractions provided
57/// in the simulation library.
58///
59/// Continuations may be *branded* using an additional lifetime that ties
60/// [Token] to specific instances. This allows composition and chaining of
61/// method calls that depend on specific [States] without having to
62/// continuously test the current states within the method implementations.
63///
64/// [futures]: Future
65/// [Token]: token
66/// [States]: State
67pub struct Continuation<'brand, C: ?Sized + Config> {
68 /// An intrusive link for insertion of a continuation into the calendar.
69 hook: Link,
70 /// Intrusive reference counter and raw pointer to the most specialized
71 /// version of the type containing this continuation.
72 ///
73 /// This is needed to provide run-time polymorphism over the type
74 /// of future being executed as well as what code is run on dropping. See
75 /// [Dispatch] for further information.
76 task_box: IrcBox<ContBox>,
77 /// Pointer to data shared across different continuations.
78 share: NonNull<Share<C>>,
79 /// A list of continuations waiting for this continuation to terminate.
80 pending: Cell<LinkedList<Adapter<C>>>,
81 /// Precedence of this continuation relative to other continuations from the
82 /// same agent.
83 prec: Cell<Prec>,
84 /// Current state of this continuation instance.
85 state: StateMachine<'brand, State<C>>,
86 /// Span information for the current continuation.
87 #[cfg(feature = "tracing")]
88 span: tracing::Span,
89}
90
91impl<C: ?Sized + Config> Continuation<'static, C> {
92 /// Creates a new continuation initialized in [State::Born] with specific
93 /// location information attached.
94 ///
95 /// [State::Born]: erased::State::Born
96 pub(crate) fn new(prec: Prec, location: &'static Location<'static>) -> Self {
97 Self {
98 hook: Link::new(),
99 task_box: IrcBox::with_location(ContBox::new(), location),
100 share: NonNull::dangling(),
101 pending: Cell::new(LinkedList::new(Adapter::NEW)),
102 prec: Cell::new(prec),
103 state: StateMachine::default(),
104 #[cfg(feature = "tracing")]
105 span: tracing::Span::current(),
106 }
107 }
108
109 /// Returns the exit code of the continuation, or `None` if it hasn't
110 /// terminated.
111 pub(crate) fn result(&self) -> Option<ExitStatus> {
112 self.brand(|task, once| {
113 task.token(once)
114 .into_term()
115 .map(|state| task.branded_result(&state))
116 .ok()
117 })
118 }
119
120 /// Reactivates the continuation at the current model-time.
121 pub(crate) fn wake(this: Irc<Self>) -> Result<(), NotIdle> {
122 this.clone().brand(|task, once| {
123 let idle = task.token(once).into_idle()?;
124
125 task.branded_share(&idle)
126 .sim()
127 .calendar()
128 .activate(task.clone(), idle);
129
130 Ok(())
131 })
132 }
133
134 /// Returns the next model time this continuation is scheduled for, or
135 /// `None` if it isn't scheduled.
136 pub(crate) fn time(&self) -> Option<C::Time> {
137 self.brand(|task, once| {
138 task.token(once)
139 .into_next()
140 .ok()
141 .map(|next| task.next_time(&next))
142 })
143 }
144}
145
146impl<'brand, C: ?Sized + Config> Continuation<'brand, C> {
147 /// Enters the [`Span`] associated with the agent owning the shared data.
148 ///
149 /// [`Span`]: tracing::Span
150 #[cfg(feature = "tracing")]
151 pub fn enter_span(&self) -> tracing::span::Entered<'_> {
152 self.span.enter()
153 }
154
155 /// No-op in lieu of entering the span associated with the agent owning the
156 /// shared data.
157 #[cfg(not(feature = "tracing"))]
158 pub const fn enter_span(&self) {}
159
160 /// Sets a new virtual function pointer for this continuation.
161 ///
162 /// # Safety
163 /// The caller is responsible to ensure that the pointer stays valid for
164 /// the duration of the continuation's life.
165 pub(crate) unsafe fn set_vptr(&self, vptr: NonNull<dyn Dispatch + '_>) {
166 unsafe {
167 self.task_box.set_vptr(vptr);
168 }
169 }
170
171 /// Clears the virtual function pointer for this continuation, preventing any
172 /// methods from being called.
173 ///
174 /// This can be used to prevent [`Dispatch::reclaim`] from being called,
175 /// even if the reference counter reaches zero.
176 pub(crate) fn clear_vptr(&self) {
177 self.task_box.clear_vptr();
178 }
179
180 /// Returns the number of references to this continuation.
181 pub(crate) fn use_count(&self) -> usize {
182 self.task_box.refs.get()
183 }
184
185 /// Returns a copy of the internal [State](erased::State).
186 pub(crate) fn state(&self) -> &StateMachine<'brand, State<C>> {
187 &self.state
188 }
189
190 /// Returns the current [Prec] of this continuation.
191 pub(crate) fn prec(&self) -> Prec {
192 self.prec.get()
193 }
194
195 /// Sets the new [Prec] of this continuation.
196 pub(crate) fn set_prec(&self, prec: Prec) {
197 self.prec.set(prec);
198 }
199
200 /// Adds another continuation to the list of continuations to be awoken upon terminating.
201 #[inline]
202 pub(crate) fn insert_pending(&self, other: Irc<Continuation<'static, C>>) {
203 let mut list = self.pending.take();
204 list.push_back(other);
205 self.pending.set(list);
206 }
207
208 /// Removes a previously added continuation from the list of pending continuations.
209 ///
210 /// # Safety
211 /// It is the caller's responsibility to ensure that the continuation had been
212 /// added previously via [Self::insert_pending].
213 pub(crate) unsafe fn remove_pending(&self, other: &Continuation<'static, C>) {
214 // Only unlink if the other continuation is actually linked right now.
215 // This can happen during panics, when the `drop` impl of a `Join`
216 // attempts to remove a stored continuation from the pending list, but
217 // it has already been removed by a prior reactivation.
218 if other.hook.is_linked() {
219 let mut list = self.pending.take();
220
221 unsafe {
222 list.cursor_mut_from_ptr(other.detach()).remove();
223 }
224
225 self.pending.set(list);
226 }
227 }
228
229 /// Awakens all pending continuations and clears the list.
230 pub(crate) fn wake_pending(&self) {
231 for task in self.pending.take() {
232 Continuation::wake(task).ok();
233 }
234 }
235
236 /// Converts the specific brand into a generic brand, breaking the
237 /// connection with the equally branded token.
238 pub(crate) fn detach(&self) -> &Continuation<'static, C> {
239 unsafe { core::mem::transmute(self) }
240 }
241
242 /// Returns the [`Location`] information for this `Continuation`.
243 pub(crate) fn location(&self) -> &'static Location<'static> {
244 IrcBox::location(&self.task_box)
245 }
246
247 /// Performs a runtime-check if this continuation has been dereferenced on the
248 /// same thread as the one the executor is running and panicks if that is
249 /// not the case.
250 ///
251 /// # Safety
252 /// This method can only be called after the `Continuation` has been
253 /// activated. Calling it is thread-safe.
254 unsafe fn is_same_thread(&self) -> bool {
255 // Extract the shared data from the task.
256 let share = unsafe { self.share.as_ref() };
257
258 // Compare the pointer-address of this continuation's simulation context
259 // to the pointer-address of the thread-local simulation context.
260 crate::erased::with(|sim| core::ptr::addr_eq(&**share.sim(), sim)).unwrap_or(false)
261 }
262
263 /// Returns an enumeration copy of the internal [State](token::State).
264 pub(crate) fn token(&self, once: Ephemeral<'brand>) -> token::State<'brand> {
265 self.state.token(once)
266 }
267
268 /// Binds a continuation to [shared data].
269 ///
270 /// # Safety
271 /// The caller is responsible to ensure that the shared-data-reference
272 /// outlives the (active) part of the continuation's life.
273 ///
274 /// [shared data]: Share
275 pub(crate) unsafe fn bind(
276 mut self: Pin<&mut Self>,
277 born: token::Born<'brand>,
278 share: &Share<C>,
279 ) -> token::Idle<'brand> {
280 // assign the reference to the shared data
281 self.share = NonNull::from(share);
282
283 // enter our `Span` to properly record the transition
284 let _span = self.enter_span();
285
286 // transition into state `Idle`
287 self.state.transition(born, ())
288 }
289
290 /// Purges the continuation from the calendar.
291 pub(crate) fn deschedule(&self, next: token::Next<'brand>) -> token::Idle<'brand> {
292 let share = self.branded_share(&next);
293
294 // remove the continuation from the calendar
295 share.sim().calendar().remove(self, next)
296 }
297
298 /// Removes the active state from the continuation.
299 pub(crate) fn deactivate(&self, busy: token::Busy<'brand>) -> token::Idle<'brand> {
300 let share = self.branded_share(&busy);
301
302 // deregister the continuation from the active cell
303 share.sim().unslot(self, busy)
304 }
305
306 /// Polls the underlying future of this continuation.
307 pub(crate) fn poll(&self, busy: token::Busy<'brand>, cx: &mut Context<'_>) -> Poll<()> {
308 // read the virtual function from the table
309
310 // SAFETY: the busy-token testifies that the continuation has been
311 // bound, which ensures that the vptr to the virtual function table has
312 // been set; pinning is ensured by the binding routine requiring it
313 let vtab = unsafe { Pin::new_unchecked(self.task_box.vptr.get().unwrap().as_ref()) };
314
315 // temporarily escape the branding to allow `Future::poll()` to rebrand
316 // without accidentally creating two tokens for the same instance
317 let (once, res) = self.state.debrand(busy, move |_| vtab.poll(cx));
318
319 // analyze the resulting state
320 match self.token(once).into_busy() {
321 Ok(busy) => match res {
322 Poll::Ready(result) => {
323 // busy -> done
324 let _: token::Done<'_> = self.state.transition(busy, result);
325
326 // reactivate pending continuations on completion
327 self.wake_pending();
328
329 Poll::Ready(())
330 }
331 Poll::Pending => {
332 // busy -> idle
333 let _: token::Idle<'_> = self.state.transition(busy, ());
334 Poll::Pending
335 }
336 },
337 Err(_err) => {
338 debug_assert!(
339 res.is_pending(),
340 "task in state `{:?}` should not have been able to terminate",
341 _err.0
342 );
343 Poll::Pending
344 }
345 }
346 }
347
348 /// Returns a reference to the shared data for this continuation.
349 pub(crate) fn branded_share<'s, I>(&'s self, init: &I) -> &'s Share<C>
350 where
351 I: Into<token::Init<'brand>>,
352 {
353 let _ = init;
354
355 // SAFETY: the token witness testifies that the shared data is
356 // initialized, which only happens during binding
357 unsafe { self.share.as_ref() }
358 }
359
360 /// Returns a reference to the shared data if initialization of this continuation
361 /// has been completed.
362 pub(crate) fn share(&self) -> Option<&Share<C>> {
363 if self.state().erased().is_init() {
364 // SAFETY: once bound, the pointer stays valid
365 Some(unsafe { self.share.as_ref() })
366 } else {
367 None
368 }
369 }
370
371 /// Grants access to the calendar state if in state [`Next`](State::Next).
372 pub(crate) fn next_state<F, R>(&self, next: &token::Next<'brand>, f: F) -> R
373 where
374 F: FnOnce(&<C::Plan as Scheduler>::State) -> R,
375 {
376 let _ = next;
377
378 // SAFETY: the `Next` token guarantees that this continuation is in the
379 // correct state
380 match &*self.state.borrow() {
381 State::Next(state) => f(state),
382 _ => unsafe { unreachable_unchecked() },
383 }
384 }
385
386 /// Returns the model time that the continuation will be activated.
387 pub(crate) fn next_time(&self, next: &token::Next<'brand>) -> C::Time {
388 self.next_state(next, |s| s.time())
389 }
390
391 /// Returns the [Cell] containing the current mark of the shared data
392 /// associated with this continuation.
393 ///
394 /// This value is used to organize different continuations with identical shared
395 /// data in the calendar such that they are executed in a contiguous batch.
396 pub(crate) fn mark<'s, I>(&'s self, init: &I) -> &'s Cell<Mark>
397 where
398 I: Into<token::Init<'brand>>,
399 {
400 self.branded_share(init).mark()
401 }
402
403 /// Returns the exit code of this continuation.
404 pub(crate) fn branded_result<T>(&self, _: &T) -> ExitStatus
405 where
406 T: Into<token::Term<'brand>>,
407 {
408 // SAFETY: the continuation is in state `Done` or `Gone` per the token witness
409 match &*self.state.borrow() {
410 State::Done(rc) | State::Gone(rc) => *rc,
411 _ => unsafe { unreachable_unchecked() },
412 }
413 }
414}
415
416// Continuations offer unique methods for Branded variants
417impl<'b, C: ?Sized + Config> Stateful for Continuation<'b, C> {
418 type Brand = &'b ();
419
420 unsafe fn enter(&self) {
421 unsafe {
422 self.state.enter();
423 }
424 }
425
426 unsafe fn leave(&self) {
427 unsafe {
428 self.state.leave();
429 }
430 }
431}
432
433impl<'b, C: ?Sized + Config> Rebrand<'b> for Continuation<'b, C> {
434 type Kind<'a> = Continuation<'a, C>;
435}
436
437// Continuations only contain pointers to pinned data and are not pinned themselves
438impl<C: ?Sized + Config> Unpin for Continuation<'_, C> {}
439
440impl<C: ?Sized + Config> fmt::Debug for Continuation<'_, C> {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 let mut s = f.debug_struct("Continuation");
443
444 s.field("state", &self.state.borrow())
445 .field("location", self.location());
446
447 self.brand(|task, once| {
448 struct PrettyName(Label);
449
450 impl fmt::Debug for PrettyName {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 write!(f, "\"{}\"", &self.0)
453 }
454 }
455
456 let state = task.token(once);
457 let init = state.as_init()?;
458
459 s.field("label", &PrettyName(task.branded_share(init).label()))
460 .field("rank", &task.branded_share(init).rank());
461
462 None::<()>
463 });
464
465 s.field("prec", &self.prec().float_range())
466 .field("refs", &self.use_count())
467 .finish()
468 }
469}
470
471impl<C: Config> crate::erased::Continuation for Continuation<'static, C> {
472 fn subject(&self) -> &dyn Any {
473 self.share().map_or(&(), |shared| shared.subject())
474 }
475
476 fn label(&self) -> Label {
477 self.share()
478 .map_or(Label::default(), |shared| shared.label())
479 }
480
481 fn prec(&self) -> Prec {
482 self.prec()
483 }
484
485 fn state(&self) -> erased::State {
486 self.state().borrow().erased()
487 }
488}
489
490unsafe impl<C: ?Sized + Config> IntrusivelyCounted for Continuation<'_, C> {
491 fn irc_box(&self) -> &IrcBox<dyn IrcBoxed> {
492 &self.task_box
493 }
494}
495
496/* ************************************************************** Irc Support */
497
498/// Container type used to support [intrusive reference counting](Irc).
499pub struct ContBox {
500 /// Contains a counter for the number of references to the outer
501 /// [`Continuation`].
502 refs: Cell<usize>,
503 /// Pointer to the most-specialized version of the instance.
504 /// Used to recover type information during dynamic dispatch.
505 vptr: Cell<Option<NonNull<dyn Dispatch>>>,
506}
507
508impl ContBox {
509 /// Creates a new irc box from a location with zero references and no
510 /// dispatcher.
511 const fn new() -> Self {
512 ContBox {
513 refs: Cell::new(0),
514 vptr: Cell::new(None),
515 }
516 }
517
518 /// Sets the pointer to the vtable.
519 ///
520 /// # Safety
521 /// It is the callers' responsibility to ensure that the `vptr` pointer to
522 /// the `dyn Dispatch` object outlives the continuation box.
523 unsafe fn set_vptr(&self, vptr: NonNull<dyn Dispatch + '_>) {
524 unsafe {
525 use core::mem::transmute;
526
527 self.vptr.set(Some(transmute::<
528 NonNull<dyn Dispatch + '_>,
529 NonNull<dyn Dispatch + 'static>,
530 >(vptr)));
531 }
532 }
533
534 /// Clears the pointer to the vtable.
535 ///
536 /// This can be useful to prevent [`IrcBoxed::release`] from returning
537 /// a destructor function in case the reference counter reaches zero.
538 fn clear_vptr(&self) {
539 self.vptr.set(None);
540 }
541}
542
543unsafe impl IrcBoxed for ContBox {
544 fn ref_count(&self) -> usize {
545 self.refs.get()
546 }
547
548 fn acquire(&self, _: crate::ptr::Private) {
549 self.refs.set(self.refs.get() + 1);
550 }
551
552 fn release(&self, _: crate::ptr::Private) {
553 self.refs.set(self.refs.get() - 1);
554 }
555
556 fn reclaim(&self, _: crate::ptr::Private) -> Option<fn(NonNull<dyn IrcBoxed>)> {
557 self.vptr.get().is_some().then_some(
558 // return a function reclaiming the outer type if no references point
559 // to the Continuation anymore; this separation is necessary to prevent
560 // overlapping references to this Continuation from self and (indirectly) from
561 // the inner dyn object
562 |this| unsafe {
563 // restore the pointer to the virtual table
564 let this = this.cast::<Self>().as_ref();
565 let mut vptr = this.vptr.get().unwrap_unchecked();
566
567 // call the reclaim-method on an exclusive reference to the dyn
568 // object; the temporary reference to the continuation has been dropped
569 // previously, and no other active references exist to the continuation
570 // which allows us to take this exclusive reference
571 Pin::new_unchecked(vptr.as_mut()).reclaim();
572 },
573 )
574 }
575}
576
577/* ************************************************************** Continuation States */
578
579fsm! {
580 /// Represents the state of a [`Continuation`] throughout its lifecycle.
581 ///
582 /// A `Continuation` starts in the [`Born`] state without references to
583 /// shared data or its future, as provided by its owning [Job] or [Agent].
584 /// After binding pinned references to it, the `Continuation` moves to the
585 /// [`Idle`] state, indicating it is unscheduled.
586 ///
587 /// From the [`Idle`] state, the `Continuation` can be scheduled by
588 /// inserting it into the event calendar at a specific model time,
589 /// transitioning it to the [`Next`] state. When the model time reaches this
590 /// point and the `Continuation` is activated, it enters the [`Busy`] state.
591 ///
592 /// Upon completion, the `Continuation` transitions to the [`Done`] state,
593 /// indicating that a return value is available for extraction. At any point
594 /// before normal termination, the `Continuation` can be aborted, moving it
595 /// to the [`Gone`] state and dropping its bound future. The [`Gone`] state
596 /// is the final state in a `Continuation`'s lifecycle.
597 ///
598 /// [Job]: crate::job::Job
599 /// [Agent]: crate::Agent
600 /// [`Born`]: State::Born
601 /// [`Idle`]: State::Idle
602 /// [`Next`]: State::Next
603 /// [`Busy`]: State::Busy
604 /// [`Done`]: State::Done
605 /// [`Gone`]: State::Gone
606 #[derive(Default)]
607 pub enum State<C: Config> {
608 /// State of a continuation signifying a non-bound future and shared data.
609 #[default]
610 Born -> {Idle, Gone},
611 /// State of a continuation that is waiting for external reactivation.
612 Idle -> {Next, Gone},
613 /// State of the continuation that is currently active. At most one
614 /// continuation may be in this state during a simulation run at any
615 /// time.
616 Busy -> {Idle, Done},
617 /// State of a continuation that is managed by the calendar.
618 Next(<C::Plan as Scheduler>::State) -> {Idle, Busy},
619 /// State of a completed continuation with a result available for extraction.
620 Done(ExitStatus) -> {Gone},
621 /// State of a continuation that cannot be scheduled anymore.
622 Gone(ExitStatus) -> {}
623 }
624
625 /// Meta-state for all non-[`Born`] states.
626 ///
627 /// [`Born`]: State::Born
628 pub Init = {Idle, Busy, Next, Done, Gone};
629
630 /// Meta-state for [`Done`] and [`Gone`] states and a subset of the [`Init`]
631 /// meta-state.
632 ///
633 /// [`Done`]: State::Done
634 /// [`Gone`]: State::Gone
635 /// [`Init`]: token::Init
636 pub Term: Init = {Done, Gone};
637}
638
639impl<C: ?Sized + Config> fmt::Debug for State<C> {
640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641 let mut debug = f.debug_tuple(self.label());
642
643 match self {
644 State::Next(plan) => debug.field(plan),
645 State::Done(exit) => debug.field(exit),
646 State::Gone(exit) => debug.field(exit),
647 _ => &mut debug,
648 }
649 .finish()
650 }
651}