veecle_os_runtime/
execute.rs

1#![expect(
2    private_bounds,
3    reason = "
4        everything defined in here except the macro are internal helpers,
5        they often mention private types
6    "
7)]
8
9use crate::Never;
10use crate::actor::{Actor, Datastore, StoreRequest};
11use crate::cons::{Cons, Nil, TupleConsToCons};
12use crate::datastore::{
13    ExclusiveReader, InitializedReader, Reader, SlotTrait, Storable, Writer, generational,
14};
15use core::any::TypeId;
16use core::pin::Pin;
17
18/// Internal helper to implement [`Datastore::slot`] recursively for a cons-list of slots.
19trait Slots {
20    /// Attempts to find a slot of the given type.
21    /// Returns None if no such slot exists.
22    fn try_slot<S>(self: Pin<&Self>) -> Option<Pin<&S>>
23    where
24        S: SlotTrait;
25}
26
27impl Slots for Nil {
28    fn try_slot<S>(self: Pin<&Self>) -> Option<Pin<&S>>
29    where
30        S: SlotTrait,
31    {
32        None
33    }
34}
35
36impl<T> Slots for T
37where
38    T: SlotTrait + core::any::Any,
39{
40    fn try_slot<S>(self: Pin<&Self>) -> Option<Pin<&S>>
41    where
42        S: SlotTrait + core::any::Any,
43    {
44        if TypeId::of::<S>() == TypeId::of::<T>() {
45            // SAFETY:
46            // `Pin::map_unchecked`: We're only transforming the type, so it retains its pinned-ness.
47            // `cast` + `as_ref`: We verified above that the types of `S` and `T` are the same.
48            Some(unsafe {
49                Pin::map_unchecked(self, |this| {
50                    core::ptr::NonNull::from_ref(this).cast::<S>().as_ref()
51                })
52            })
53        } else {
54            None
55        }
56    }
57}
58
59impl<U, R> Slots for Cons<U, R>
60where
61    U: Slots,
62    R: Slots,
63{
64    fn try_slot<S>(self: Pin<&Self>) -> Option<Pin<&S>>
65    where
66        S: SlotTrait,
67    {
68        let this = self.project_ref();
69
70        this.0.try_slot::<S>().or_else(|| this.1.try_slot::<S>())
71    }
72}
73
74/// Internal helper to construct runtime slot instances from a type-level cons list of slots.
75trait IntoSlots {
76    /// The same cons-list type, used to construct slot instances.
77    type Slots: Slots;
78
79    /// Creates a new instance of the slot cons-list with all slots empty.
80    fn make_slots() -> Self::Slots;
81
82    /// Validates all slots in this cons-list against the actor access patterns.
83    fn validate_all<'a, A>()
84    where
85        A: ActorList<'a>;
86}
87
88impl IntoSlots for Nil {
89    type Slots = Nil;
90
91    fn make_slots() -> Self::Slots {
92        Nil
93    }
94
95    fn validate_all<'a, A>()
96    where
97        A: ActorList<'a>,
98    {
99    }
100}
101
102impl<S> IntoSlots for S
103where
104    S: SlotTrait + 'static,
105{
106    type Slots = S;
107
108    fn make_slots() -> Self::Slots {
109        S::new()
110    }
111
112    fn validate_all<'a, A>()
113    where
114        A: ActorList<'a>,
115    {
116        let type_id = S::data_type_id();
117
118        S::validate_access_pattern(
119            (A::writers_count(type_id), A::writers(type_id)),
120            (
121                A::exclusive_readers_count(type_id),
122                A::exclusive_readers(type_id),
123            ),
124            (
125                A::non_exclusive_readers_count(type_id),
126                A::non_exclusive_readers(type_id),
127            ),
128        );
129    }
130}
131
132impl<S, R> IntoSlots for Cons<S, R>
133where
134    S: IntoSlots,
135    R: IntoSlots,
136{
137    type Slots = Cons<S::Slots, R::Slots>;
138
139    fn make_slots() -> Self::Slots {
140        Cons(S::make_slots(), R::make_slots())
141    }
142
143    fn validate_all<'a, A>()
144    where
145        A: ActorList<'a>,
146    {
147        S::validate_all::<'a, A>();
148        R::validate_all::<'a, A>();
149    }
150}
151
152#[allow(
153    rustdoc::private_intra_doc_links,
154    reason = "
155        This lint is hit when documenting with `--document-private-items`.
156        If we use `expect`, a warning is emitted when not using `--document-private-items`.
157        If we remove the lint, a warning is emitted when using `--document-private-items`.
158        To be able to deny warning, we need to allow the lint here.
159        https://github.com/rust-lang/rust/issues/145449
160    "
161)]
162/// Given a slot cons-list, combines it with a [`generational::Source`] to implement [`Datastore`].
163impl<S: Slots> Datastore for Cons<generational::Source, S>
164where
165    S: Slots,
166{
167    fn source(self: Pin<&Self>) -> Pin<&generational::Source> {
168        let this = self.project_ref();
169        this.0
170    }
171
172    fn slot<T>(self: Pin<&Self>, requestor: &'static str) -> Pin<&T>
173    where
174        T: SlotTrait,
175    {
176        let this = self.project_ref();
177        this.1.try_slot::<T>().unwrap_or_else(|| {
178            panic!(
179                "no slot available for `{}`, required by `{requestor}`",
180                T::data_type_name()
181            )
182        })
183    }
184}
185
186/// Given a cons-list of slot types, returns a complete [`Datastore`] that contains those slots.
187pub(crate) fn make_store<T>() -> impl Datastore
188where
189    T: IntoSlots,
190{
191    Cons(generational::Source::new(), T::make_slots())
192}
193
194/// Internal helper to query how a [`StoreRequest`] type will use a specific type.
195pub trait AccessKind {
196    /// Returns whether this is a writer.
197    fn writer(_type_id: TypeId) -> bool {
198        false
199    }
200
201    /// Returns whether this is a reader (both exclusive and non-exclusive).
202    fn reader(_type_id: TypeId) -> bool {
203        false
204    }
205
206    /// Returns whether this is an exclusive reader.
207    fn exclusive_reader(_type_id: TypeId) -> bool {
208        false
209    }
210}
211
212impl<T> AccessKind for Writer<'_, T>
213where
214    T: Storable + 'static,
215{
216    fn writer(type_id: TypeId) -> bool {
217        type_id == TypeId::of::<T>()
218    }
219}
220
221impl<T> AccessKind for Reader<'_, T>
222where
223    T: Storable + 'static,
224{
225    fn reader(type_id: TypeId) -> bool {
226        type_id == TypeId::of::<T>()
227    }
228}
229
230impl<T> AccessKind for InitializedReader<'_, T>
231where
232    T: Storable + 'static,
233{
234    fn reader(type_id: TypeId) -> bool {
235        type_id == TypeId::of::<T>()
236    }
237}
238
239impl<T> AccessKind for ExclusiveReader<'_, T>
240where
241    T: Storable + 'static,
242{
243    fn reader(type_id: TypeId) -> bool {
244        type_id == TypeId::of::<T>()
245    }
246
247    fn exclusive_reader(type_id: TypeId) -> bool {
248        type_id == TypeId::of::<T>()
249    }
250}
251
252/// Internal helper to query how a cons-lists of [`StoreRequest`] types will use a specific type.
253pub trait AccessCount {
254    /// Returns how many writers for the given type exist in this list.
255    fn writers(type_id: TypeId) -> usize;
256
257    /// Returns how many readers for the given type exist in this list (both exclusive and non-exclusive).
258    fn readers(type_id: TypeId) -> usize;
259
260    /// Returns how many exclusive readers for the given type exist in this list.
261    fn exclusive_readers(type_id: TypeId) -> usize;
262}
263
264impl AccessCount for Nil {
265    fn writers(_type_id: TypeId) -> usize {
266        0
267    }
268
269    fn readers(_type_id: TypeId) -> usize {
270        0
271    }
272
273    fn exclusive_readers(_type_id: TypeId) -> usize {
274        0
275    }
276}
277
278impl<T, U> AccessCount for Cons<T, U>
279where
280    T: AccessKind,
281    U: AccessCount,
282{
283    fn writers(type_id: TypeId) -> usize {
284        (if T::writer(type_id) { 1 } else { 0 }) + U::writers(type_id)
285    }
286
287    fn readers(type_id: TypeId) -> usize {
288        (if T::reader(type_id) { 1 } else { 0 }) + U::readers(type_id)
289    }
290
291    fn exclusive_readers(type_id: TypeId) -> usize {
292        (if T::exclusive_reader(type_id) { 1 } else { 0 }) + U::exclusive_readers(type_id)
293    }
294}
295
296/// Internal helper to access details about a cons-list of actors so they can be validated against a store.
297pub(crate) trait ActorList<'a>
298where
299    Self: 'a,
300{
301    /// A cons-list of init-contexts for this cons-list (essentially `self.map(|actor| actor.init_context)`).
302    type InitContexts;
303
304    /// A cons-list of slot cons-lists for this actor list (nested structure).
305    type AllSlots;
306
307    /// Returns the number of writers for the given type in this actor list.
308    fn writers_count(type_id: TypeId) -> usize;
309
310    /// Returns the number of exclusive readers for the given type in this actor list.
311    fn exclusive_readers_count(type_id: TypeId) -> usize;
312
313    /// Returns the number of non-exclusive readers for the given type in this actor list.
314    fn non_exclusive_readers_count(type_id: TypeId) -> usize;
315
316    /// Returns the type names of the actors in this list that write to the given type.
317    ///
318    /// If an actor has multiple writers for the same type it will be in the list multiple times.
319    fn writers(type_id: TypeId) -> impl Iterator<Item = &'static str>;
320
321    /// Returns the type names of the actors in this list that read from the given type with an
322    /// exclusive reader.
323    ///
324    /// If an actor has multiple exclusive readers for the same type it will be in the list multiple
325    /// times.
326    fn exclusive_readers(type_id: TypeId) -> impl Iterator<Item = &'static str>;
327
328    /// Returns the type names of the actors in this list that read from the given type with a
329    /// non-exclusive reader.
330    ///
331    /// If an actor has multiple non-exclusive readers for the same type it will be in the list
332    /// multiple times.
333    fn non_exclusive_readers(type_id: TypeId) -> impl Iterator<Item = &'static str>;
334}
335
336impl ActorList<'_> for Nil {
337    type InitContexts = Nil;
338    type AllSlots = Nil;
339
340    fn writers_count(_type_id: TypeId) -> usize {
341        0
342    }
343
344    fn exclusive_readers_count(_type_id: TypeId) -> usize {
345        0
346    }
347
348    fn non_exclusive_readers_count(_type_id: TypeId) -> usize {
349        0
350    }
351
352    fn writers(_type_id: TypeId) -> impl Iterator<Item = &'static str> {
353        core::iter::empty()
354    }
355
356    fn exclusive_readers(_type_id: TypeId) -> impl Iterator<Item = &'static str> {
357        core::iter::empty()
358    }
359
360    fn non_exclusive_readers(_type_id: TypeId) -> impl Iterator<Item = &'static str> {
361        core::iter::empty()
362    }
363}
364
365impl<'a, T, U> ActorList<'a> for Cons<T, U>
366where
367    T: Actor<'a, StoreRequest: TupleConsToCons> + 'a,
368    U: ActorList<'a> + 'a,
369    <<T as Actor<'a>>::StoreRequest as TupleConsToCons>::Cons: AccessCount,
370{
371    /// For `Actor::InitContext` we just need to map directly to the associated type.
372    type InitContexts = Cons<<T as Actor<'a>>::InitContext, <U as ActorList<'a>>::InitContexts>;
373
374    /// For `AllSlots` we create a cons list of each actor's slots (nested structure).
375    type AllSlots = Cons<<T as Actor<'a>>::Slots, <U as ActorList<'a>>::AllSlots>;
376
377    fn writers_count(type_id: TypeId) -> usize {
378        <T::StoreRequest as TupleConsToCons>::Cons::writers(type_id) + U::writers_count(type_id)
379    }
380
381    fn exclusive_readers_count(type_id: TypeId) -> usize {
382        <T::StoreRequest as TupleConsToCons>::Cons::exclusive_readers(type_id)
383            + U::exclusive_readers_count(type_id)
384    }
385
386    fn non_exclusive_readers_count(type_id: TypeId) -> usize {
387        (<T::StoreRequest as TupleConsToCons>::Cons::readers(type_id)
388            - <T::StoreRequest as TupleConsToCons>::Cons::exclusive_readers(type_id))
389            + U::non_exclusive_readers_count(type_id)
390    }
391
392    fn writers(type_id: TypeId) -> impl Iterator<Item = &'static str> {
393        core::iter::repeat_n(
394            core::any::type_name::<T>(),
395            <T::StoreRequest as TupleConsToCons>::Cons::writers(type_id),
396        )
397        .chain(U::writers(type_id))
398    }
399
400    fn exclusive_readers(type_id: TypeId) -> impl Iterator<Item = &'static str> {
401        core::iter::repeat_n(
402            core::any::type_name::<T>(),
403            <T::StoreRequest as TupleConsToCons>::Cons::exclusive_readers(type_id),
404        )
405        .chain(U::exclusive_readers(type_id))
406    }
407
408    fn non_exclusive_readers(type_id: TypeId) -> impl Iterator<Item = &'static str> {
409        core::iter::repeat_n(
410            core::any::type_name::<T>(),
411            <T::StoreRequest as TupleConsToCons>::Cons::readers(type_id)
412                - <T::StoreRequest as TupleConsToCons>::Cons::exclusive_readers(type_id),
413        )
414        .chain(U::non_exclusive_readers(type_id))
415    }
416}
417
418/// Creates a store and validates actors in a single call to enable type inference.
419///
420/// This function combines store creation and validation so that the actor list type parameter appears only once,
421/// allowing Rust's type inference to work across both operations.
422///
423/// The slots are computed from the actor list's associated type.
424pub fn make_store_and_validate<'a, A, I>(init_contexts: I) -> (impl Datastore + 'a, I)
425where
426    A: ActorList<'a, InitContexts = I>,
427    A::AllSlots: IntoSlots,
428{
429    let store = make_store::<A::AllSlots>();
430
431    A::AllSlots::validate_all::<'a, A>();
432
433    (store, init_contexts)
434}
435
436/// Internal helper to get a full future that initializes and executes an [`Actor`] given a [`Datastore`]
437pub async fn execute_actor<'a, A>(
438    store: Pin<&'a impl Datastore>,
439    init_context: A::InitContext,
440) -> Never
441where
442    A: Actor<'a>,
443{
444    let requestor = core::any::type_name::<A>();
445    veecle_telemetry::future::FutureExt::with_span(
446        async move {
447            match A::new(
448                A::StoreRequest::request(store, requestor).await,
449                init_context,
450            )
451            .run()
452            .await
453            {
454                Err(error) => panic!("{error}"),
455            }
456        },
457        veecle_telemetry::span!("actor", actor = core::any::type_name::<A>()),
458    )
459    .await
460}
461
462/// Execute a given set of actors without heap allocation.
463///
464/// ```rust
465/// use core::fmt::Debug;
466///
467/// use veecle_os_runtime::{Never, Reader, Storable, Writer};
468///
469/// #[derive(Debug, Clone, PartialEq, Eq, Default, Storable)]
470/// pub struct Ping {
471///     value: u32,
472/// }
473///
474/// #[derive(Debug, Clone, PartialEq, Eq, Default, Storable)]
475/// pub struct Pong {
476///     value: u32,
477/// }
478///
479/// #[veecle_os_runtime::actor]
480/// async fn ping_actor(mut ping: Writer<'_, Ping>, pong: Reader<'_, Pong>) -> Never {
481///     let mut value = 0;
482///     ping.write(Ping { value }).await;
483///
484///     let mut pong = pong.wait_init().await;
485///     loop {
486///         ping.write(Ping { value }).await;
487///         value += 1;
488///
489///         pong.wait_for_update().await.read(|pong| {
490///             println!("Pong: {}", pong.value);
491///         });
492/// #       // Exit the application to allow doc-tests to complete.
493/// #       std::process::exit(0);
494///     }
495/// }
496///
497/// #[veecle_os_runtime::actor]
498/// async fn pong_actor(mut pong: Writer<'_, Pong>, ping: Reader<'_, Ping>) -> Never {
499///     let mut ping = ping.wait_init().await;
500///     loop {
501///         let ping = ping.wait_for_update().await.read_cloned();
502///         println!("Ping: {}", ping.value);
503///
504///         let data = Pong { value: ping.value };
505///         pong.write(data).await;
506///     }
507/// }
508///
509/// futures::executor::block_on(
510///    veecle_os_runtime::execute! {
511///        actors: [PingActor, PongActor],
512///    }
513/// )
514#[macro_export]
515macro_rules! execute {
516    (
517        actors: [
518            $($actor_type:ty $(: $init_context:expr )? ),* $(,)?
519        ] $(,)?
520    ) => {{
521        async {
522            let (store, init_contexts) = {
523                let (store, init_contexts) = $crate::__exports::make_store_and_validate::<
524                    $crate::__make_cons!(@type $($actor_type,)*),
525                    _,
526                >($crate::__make_cons!(@value $(
527                    // Wrapper block is used to provide a `()` if no expression is passed.
528                    { $($init_context)? },
529                )*));
530                (core::pin::pin!(store), init_contexts)
531            };
532
533            let store = store.as_ref();
534
535            // To count how many actors there are, we create an array of `()` with the appropriate length.
536            const LEN: usize = [$($crate::discard_to_unit!($actor_type),)*].len();
537
538            let futures: [core::pin::Pin<&mut dyn core::future::Future<Output = $crate::Never>>; LEN] =
539                $crate::make_futures! {
540                    init_contexts: init_contexts,
541                    store: store,
542                    actors: [$($actor_type,)*],
543                };
544
545            static SHARED: $crate::__exports::ExecutorShared<LEN>
546                = $crate::__exports::ExecutorShared::new(&SHARED);
547
548            let executor = $crate::__exports::Executor::new(
549                &SHARED,
550                $crate::__exports::Datastore::source(store),
551                futures,
552            );
553
554            executor.run().await
555        }
556    }};
557}
558
559/// Internal helper to construct an array of pinned futures for given actors + init-contexts + store.
560///
561/// Returns essentially `[Pin<&mut dyn Future<Output = Never>; actors.len()]`, but likely needs annotation at the
562/// use site to force the unsize coercion.
563#[doc(hidden)]
564#[macro_export]
565macro_rules! make_futures {
566    (
567        // A cons-list of init-contexts for the passed actors.
568        init_contexts: $init_contexts:expr,
569        store: $store:expr,
570        actors: [
571            $($types:ty,)*
572        ],
573    ) => {
574        $crate::make_futures! {
575            init_contexts: $init_contexts,
576            store: $store,
577            done: [],
578            todo: [$($types,)*],
579            futures: [],
580        }
581    };
582
583    // When there are no more actors, just return the futures as an array.
584    (
585        init_contexts: $init_contexts:expr,
586        store: $store:expr,
587        done: [$($done:ty,)*],
588        todo: [],
589        futures: [
590            $($futures:expr,)*
591        ],
592    ) => {
593        [$($futures,)*]
594    };
595
596    // For each actor, add an element to the futures array, using the already done actors as the depth to read from the
597    // init-contexts cons-list. Then push this actor onto the done list so that the next actor will read deeper from the
598    // init-contexts.
599    (
600        init_contexts: $init_contexts:expr,
601        store: $store:expr,
602        done: [$($done:ty,)*],
603        todo: [$current:ty, $($todo:ty,)*],
604        futures: [
605            $($futures:expr,)*
606        ],
607    ) => {
608        $crate::make_futures! {
609            init_contexts: $init_contexts,
610            store: $store,
611            done: [$($done,)* $current,],
612            todo: [$($todo,)*],
613            futures: [
614                $($futures,)*
615                core::pin::pin!(
616                    $crate::__exports::execute_actor::<$current>(
617                        $store,
618                        $crate::__read_cons! {
619                            from: $init_contexts,
620                            depth: [$($done)*],
621                        },
622                    )
623                ),
624            ],
625        }
626    };
627}
628
629#[doc(hidden)]
630#[macro_export]
631macro_rules! discard_to_unit {
632    ($_:tt) => {
633        ()
634    };
635}
636
637#[cfg(test)]
638#[cfg_attr(coverage_nightly, coverage(off))]
639mod tests {
640    use core::marker::PhantomData;
641    use core::pin::pin;
642
643    use crate::actor::Datastore;
644    use crate::cons::Cons;
645    use crate::cons::Nil;
646    use crate::datastore::Slot;
647    use crate::execute::generational::Source;
648
649    #[test]
650    #[should_panic(
651        expected = "no slot available for `veecle_os_runtime::execute::tests::nil_slot_panics_with_correct_message::TestType`, required by `test_requestor`"
652    )]
653    fn nil_slot_panics_with_correct_message() {
654        #[derive(Debug, crate::datastore::Storable)]
655        #[storable(crate = crate)]
656        struct TestType;
657
658        let nil = pin!(Cons(Source::new(), Nil));
659        let _slot: core::pin::Pin<&Slot<TestType>> =
660            Datastore::slot(nil.as_ref(), "test_requestor");
661    }
662
663    #[test]
664    #[should_panic(expected = "type inference works")]
665    fn type_inference_for_generic_actors() {
666        use crate::{Actor, Never};
667
668        struct GenericActor<T> {
669            _phantom: PhantomData<T>,
670        }
671
672        impl<'a, T> Actor<'a> for GenericActor<T>
673        where
674            T: core::fmt::Debug + 'static,
675        {
676            type StoreRequest = ();
677            type InitContext = T;
678            type Slots = Nil;
679            type Error = Never;
680
681            fn new((): Self::StoreRequest, _context: Self::InitContext) -> Self {
682                Self {
683                    _phantom: PhantomData,
684                }
685            }
686
687            async fn run(self) -> Result<Never, Self::Error> {
688                panic!("type inference works");
689            }
690        }
691
692        futures::executor::block_on(crate::execute! {
693            actors: [
694                GenericActor<_>: 42_i32,
695            ],
696        });
697    }
698}