veecle_os_runtime/
execute.rs

1// Everything defined in here except the macro are internal helpers, they often mention private types.
2#![expect(private_bounds)]
3#![expect(private_interfaces)]
4
5use crate::actor::{Actor, Datastore, StoreRequest};
6use crate::cons::{Cons, Nil, TupleConsToCons};
7use crate::datastore::{
8    ExclusiveReader, InitializedReader, Reader, Slot, Storable, Writer, generational,
9};
10use core::any::TypeId;
11use core::pin::Pin;
12
13/// Internal helper to implement [`Datastore::slot`] recursively for a cons-list of slots.
14trait Slots {
15    /// See [`Datastore::slot`].
16    fn slot<T>(self: Pin<&Self>) -> Pin<&Slot<T>>
17    where
18        T: Storable + 'static;
19
20    /// Returns the [`TypeId`] and type names for all the slots stored in this type.
21    fn all_slots() -> impl Iterator<Item = (TypeId, &'static str)>;
22}
23
24impl Slots for Nil {
25    fn slot<T>(self: Pin<&Self>) -> Pin<&Slot<T>>
26    where
27        T: Storable + 'static,
28    {
29        panic!("no slot available for `{}`", core::any::type_name::<T>())
30    }
31
32    fn all_slots() -> impl Iterator<Item = (TypeId, &'static str)> {
33        core::iter::empty()
34    }
35}
36
37impl<U, R> Slots for Cons<Slot<U>, R>
38where
39    U: Storable + 'static,
40    R: Slots,
41{
42    fn slot<T>(self: Pin<&Self>) -> Pin<&Slot<T>>
43    where
44        T: Storable + 'static,
45    {
46        let this = self.project_ref();
47        if TypeId::of::<U>() == TypeId::of::<T>() {
48            this.0.assert_is_type()
49        } else {
50            this.1.slot::<T>()
51        }
52    }
53
54    fn all_slots() -> impl Iterator<Item = (TypeId, &'static str)> {
55        R::all_slots().chain(core::iter::once((
56            TypeId::of::<U>(),
57            core::any::type_name::<U>(),
58        )))
59    }
60}
61
62/// Internal helper to take a cons-list of `Storable` types and return a cons-list of slots for them.
63trait IntoSlots {
64    /// A cons-list that contains a slot for every type in this cons-list.
65    type Slots: Slots;
66
67    /// Creates a new instance of the slot cons-list with all slots empty.
68    fn make_slots() -> Self::Slots;
69}
70
71impl IntoSlots for Nil {
72    type Slots = Nil;
73
74    fn make_slots() -> Self::Slots {
75        Nil
76    }
77}
78
79impl<T, R> IntoSlots for Cons<T, R>
80where
81    T: Storable + 'static,
82    R: IntoSlots,
83{
84    type Slots = Cons<Slot<T>, R::Slots>;
85
86    fn make_slots() -> Self::Slots {
87        Cons(Slot::<T>::new(), R::make_slots())
88    }
89}
90
91// This lint is hit when documenting with `--document-private-items`.
92// If we use `expect`, a warning is emitted when not using `--document-private-items`.
93// If we remove the lint, a warning is emitted when using `--document-private-items`.
94// To be able to deny warning, we need to allow the lint here.
95// https://github.com/rust-lang/rust/issues/145449
96#[allow(rustdoc::private_intra_doc_links)]
97/// Given a slot cons-list, combines it with a [`generational::Source`] to implement [`Datastore`].
98impl<S: Slots> Datastore for Cons<generational::Source, S>
99where
100    S: Slots,
101{
102    fn source(self: Pin<&Self>) -> Pin<&generational::Source> {
103        let this = self.project_ref();
104        this.0
105    }
106
107    fn slot<T>(self: Pin<&Self>) -> Pin<&Slot<T>>
108    where
109        T: Storable + 'static,
110    {
111        let this = self.project_ref();
112        this.1.slot::<T>()
113    }
114}
115
116/// Given a cons-list of [`Storable`] types, returns a complete [`Datastore`] that contains a slot for each type.
117pub fn make_store<T>() -> impl Datastore
118where
119    T: IntoSlots,
120{
121    Cons(generational::Source::new(), T::make_slots())
122}
123
124/// Internal helper to query how a [`StoreRequest`] type will use a specific type.
125pub trait AccessKind {
126    /// Returns whether this is a writer.
127    fn writer(_type_id: TypeId) -> bool {
128        false
129    }
130
131    /// Returns whether this is a reader (both exclusive and non-exclusive).
132    fn reader(_type_id: TypeId) -> bool {
133        false
134    }
135
136    /// Returns whether this is an exclusive reader.
137    fn exclusive_reader(_type_id: TypeId) -> bool {
138        false
139    }
140}
141
142impl<T> AccessKind for Writer<'_, T>
143where
144    T: Storable + 'static,
145{
146    fn writer(type_id: TypeId) -> bool {
147        type_id == TypeId::of::<T>()
148    }
149}
150
151impl<T> AccessKind for Reader<'_, T>
152where
153    T: Storable + 'static,
154{
155    fn reader(type_id: TypeId) -> bool {
156        type_id == TypeId::of::<T>()
157    }
158}
159
160impl<T> AccessKind for InitializedReader<'_, T>
161where
162    T: Storable + 'static,
163{
164    fn reader(type_id: TypeId) -> bool {
165        type_id == TypeId::of::<T>()
166    }
167}
168
169impl<T> AccessKind for ExclusiveReader<'_, T>
170where
171    T: Storable + 'static,
172{
173    fn reader(type_id: TypeId) -> bool {
174        type_id == TypeId::of::<T>()
175    }
176
177    fn exclusive_reader(type_id: TypeId) -> bool {
178        type_id == TypeId::of::<T>()
179    }
180}
181
182/// Internal helper to query how a cons-lists of [`StoreRequest`] types will use a specific type.
183pub trait AccessCount {
184    /// Returns how many writers for the given type exist in this list.
185    fn writers(type_id: TypeId) -> usize;
186
187    /// Returns how many readers for the given type exist in this list (both exclusive and non-exclusive).
188    fn readers(type_id: TypeId) -> usize;
189
190    /// Returns how many exclusive readers for the given type exist in this list.
191    fn exclusive_readers(type_id: TypeId) -> usize;
192}
193
194impl AccessCount for Nil {
195    fn writers(_type_id: TypeId) -> usize {
196        0
197    }
198
199    fn readers(_type_id: TypeId) -> usize {
200        0
201    }
202
203    fn exclusive_readers(_type_id: TypeId) -> usize {
204        0
205    }
206}
207
208impl<T, U> AccessCount for Cons<T, U>
209where
210    T: AccessKind,
211    U: AccessCount,
212{
213    fn writers(type_id: TypeId) -> usize {
214        (if T::writer(type_id) { 1 } else { 0 }) + U::writers(type_id)
215    }
216
217    fn readers(type_id: TypeId) -> usize {
218        (if T::reader(type_id) { 1 } else { 0 }) + U::readers(type_id)
219    }
220
221    fn exclusive_readers(type_id: TypeId) -> usize {
222        (if T::exclusive_reader(type_id) { 1 } else { 0 }) + U::exclusive_readers(type_id)
223    }
224}
225
226/// Internal helper to query how a cons-list of cons-lists of [`StoreRequest`] types will use a specific type.
227pub trait NestedAccessCount {
228    /// Returns how many writers for the given type exist in this list of lists.
229    fn writers(type_id: TypeId) -> usize;
230
231    /// Returns how many readers for the given type exist in this list of lists (both exclusive and
232    /// non-exclusive).
233    fn readers(type_id: TypeId) -> usize;
234
235    /// Returns how many exclusive readers for the given type exist in this list of lists.
236    fn exclusive_readers(type_id: TypeId) -> usize;
237}
238
239impl NestedAccessCount for Nil {
240    fn writers(_type_id: TypeId) -> usize {
241        0
242    }
243
244    fn readers(_type_id: TypeId) -> usize {
245        0
246    }
247
248    fn exclusive_readers(_type_id: TypeId) -> usize {
249        0
250    }
251}
252
253impl<T, U> NestedAccessCount for Cons<T, U>
254where
255    T: AccessCount,
256    U: NestedAccessCount,
257{
258    fn writers(type_id: TypeId) -> usize {
259        T::writers(type_id) + U::writers(type_id)
260    }
261
262    fn readers(type_id: TypeId) -> usize {
263        T::readers(type_id) + U::readers(type_id)
264    }
265
266    fn exclusive_readers(type_id: TypeId) -> usize {
267        T::exclusive_readers(type_id) + U::exclusive_readers(type_id)
268    }
269}
270
271/// Internal helper to access details about a cons-list of actors so they can be validated against a store.
272pub trait ActorList<'a> {
273    /// A cons-list-of-cons-list-of-store-requests for this cons-list (essentially `self.map(|actor| actor.store_request)`
274    /// where each actor has a cons-list of store-requests).
275    type StoreRequests: NestedAccessCount;
276
277    /// A cons-list of init-contexts for this cons-list (essentially `self.map(|actor| actor.init_context)`).
278    type InitContexts;
279}
280
281impl ActorList<'_> for Nil {
282    type StoreRequests = Nil;
283    type InitContexts = Nil;
284}
285
286impl<'a, T, U> ActorList<'a> for Cons<T, U>
287where
288    T: Actor<'a, StoreRequest: TupleConsToCons>,
289    U: ActorList<'a>,
290    <<T as Actor<'a>>::StoreRequest as TupleConsToCons>::Cons: AccessCount,
291{
292    /// `Actor::StoreRequest` for the `#[actor]` generated types is a tuple-cons-list, for each actor in this list
293    /// convert its store requests into our nominal cons-list.
294    ///
295    /// This doesn't work with manual `Actor` implementations that have non-tuple-cons-list `StoreRequest`s.
296    type StoreRequests = Cons<
297        <<T as Actor<'a>>::StoreRequest as TupleConsToCons>::Cons,
298        <U as ActorList<'a>>::StoreRequests,
299    >;
300
301    /// For `Actor::InitContext` we just need to map directly to the associated type.
302    type InitContexts = Cons<<T as Actor<'a>>::InitContext, <U as ActorList<'a>>::InitContexts>;
303}
304
305/// Internal helper that for given sets of actors and slots validates the guarantees around slot access that we want to
306/// always uphold.
307///
308/// `init_contexts` is a cons-list of the init-context values for the actors in `A`. It is required to be passed here to
309/// drive type-inference for `A` but then just returned.
310///
311/// `_store` is the reference to the store the actors will use. A copy is passed in here as the lifetime of this
312/// reference may be required for the init-contexts inference.
313pub fn validate_actors<'a, A, S, I>(init_contexts: I, _store: Pin<&'a impl Datastore>) -> I
314where
315    A: ActorList<'a, InitContexts = I>,
316    S: IntoSlots,
317{
318    for (type_id, type_name) in S::Slots::all_slots() {
319        assert!(
320            A::StoreRequests::writers(type_id) > 0,
321            "missing writer for `{type_name}`",
322        );
323        assert!(
324            A::StoreRequests::readers(type_id) > 0,
325            "missing reader for `{type_name}`",
326        );
327        assert!(
328            A::StoreRequests::writers(type_id) == 1,
329            "multiple writers for `{type_name}`",
330        );
331        if A::StoreRequests::exclusive_readers(type_id) > 0 {
332            assert!(
333                A::StoreRequests::readers(type_id) == 1,
334                "conflict with exclusive reader for `{type_name}`",
335            );
336        }
337    }
338
339    init_contexts
340}
341
342/// Internal helper to get a full future that initializes and executes an [`Actor`] given a [`Datastore`]
343pub async fn execute_actor<'a, A>(
344    store: Pin<&'a impl Datastore>,
345    init_context: A::InitContext,
346) -> core::convert::Infallible
347where
348    A: Actor<'a>,
349{
350    let future = A::new(A::StoreRequest::request(store).await, init_context).run();
351
352    #[cfg(feature = "veecle-telemetry")]
353    let future = veecle_telemetry::future::FutureExt::with_span(
354        future,
355        veecle_telemetry::root_span!("actor", actor = core::any::type_name::<A>()),
356    );
357
358    match future.await {
359        Err(error) => panic!("{error}"),
360    }
361}
362
363/// Execute a given set of actors without heap allocation.
364///
365/// ```rust
366/// use core::convert::Infallible;
367/// use core::fmt::Debug;
368///
369/// use veecle_os_runtime::{Reader, Storable, Writer};
370///
371/// #[derive(Debug, Clone, PartialEq, Eq, Default, Storable)]
372/// pub struct Ping {
373///     value: u32,
374/// }
375///
376/// #[derive(Debug, Clone, PartialEq, Eq, Default, Storable)]
377/// pub struct Pong {
378///     value: u32,
379/// }
380///
381/// #[veecle_os_runtime::actor]
382/// async fn ping_actor(mut ping: Writer<'_, Ping>, pong: Reader<'_, Pong>) -> Infallible {
383///     let mut value = 0;
384///     ping.write(Ping { value }).await;
385///
386///     let mut pong = pong.wait_init().await;
387///     loop {
388///         ping.write(Ping { value }).await;
389///         value += 1;
390///
391///         pong.wait_for_update().await.read(|pong| {
392///             println!("Pong: {}", pong.value);
393///         });
394/// #       // Exit the application to allow doc-tests to complete.
395/// #       std::process::exit(0);
396///     }
397/// }
398///
399/// #[veecle_os_runtime::actor]
400/// async fn pong_actor(mut pong: Writer<'_, Pong>, ping: Reader<'_, Ping>) -> Infallible {
401///     let mut ping = ping.wait_init().await;
402///     loop {
403///         let ping = ping.wait_for_update().await.read_cloned();
404///         println!("Ping: {}", ping.value);
405///
406///         let data = Pong { value: ping.value };
407///         pong.write(data).await;
408///     }
409/// }
410///
411/// futures::executor::block_on(
412///    veecle_os_runtime::execute! {
413///        store: [Ping, Pong],
414///        actors: [PingActor, PongActor],
415///    }
416/// )
417#[macro_export]
418macro_rules! execute {
419    (
420        store: [
421            $($data_type:ty),* $(,)?
422        ],
423        actors: [
424            $($actor_type:ty $(: $init_context:expr )? ),* $(,)?
425        ] $(,)?
426    ) => {{
427        async {
428            let store = core::pin::pin!(
429                $crate::__exports::make_store::<$crate::__make_cons!(@type $($data_type,)*)>(),
430            );
431            let store = store.as_ref();
432
433            let init_contexts = $crate::__exports::validate_actors::<
434                $crate::__make_cons!(@type $($actor_type,)*),
435                $crate::__make_cons!(@type $($data_type,)*),
436                _,
437            >($crate::__make_cons!(@value $(
438                // Wrapper block is used to provide a `()` if no expression is passed.
439                { $($init_context)? },
440            )*), store);
441
442            // To count how many actors there are, we create an array of `()` with the appropriate length.
443            const LEN: usize = [$($crate::discard_to_unit!($actor_type),)*].len();
444
445            let futures: [core::pin::Pin<&mut dyn core::future::Future<Output = core::convert::Infallible>>; LEN] =
446                $crate::make_futures! {
447                    init_contexts: init_contexts,
448                    store: store,
449                    actors: [$($actor_type,)*],
450                };
451
452            static SHARED: $crate::__exports::ExecutorShared<LEN>
453                = $crate::__exports::ExecutorShared::new(&SHARED);
454
455            let executor = $crate::__exports::Executor::new(
456                &SHARED,
457                $crate::__exports::Datastore::source(store),
458                futures,
459            );
460
461            executor.run().await
462        }
463    }};
464}
465
466/// Internal helper to construct an array of pinned futures for given actors + init-contexts + store.
467///
468/// Returns essentially `[Pin<&mut dyn Future<Output = Infallible>; actors.len()]`, but likely needs annotation at the
469/// use site to force the unsize coercion.
470#[doc(hidden)]
471#[macro_export]
472macro_rules! make_futures {
473    (
474        // A cons-list of init-contexts for the passed actors.
475        init_contexts: $init_contexts:expr,
476        store: $store:expr,
477        actors: [
478            $($types:ty,)*
479        ],
480    ) => {
481        $crate::make_futures! {
482            init_contexts: $init_contexts,
483            store: $store,
484            done: [],
485            todo: [$($types,)*],
486            futures: [],
487        }
488    };
489
490    // When there are no more actors, just return the futures as an array.
491    (
492        init_contexts: $init_contexts:expr,
493        store: $store:expr,
494        done: [$($done:ty,)*],
495        todo: [],
496        futures: [
497            $($futures:expr,)*
498        ],
499    ) => {
500        [$($futures,)*]
501    };
502
503    // For each actor, add an element to the futures array, using the already done actors as the depth to read from the
504    // init-contexts cons-list. Then push this actor onto the done list so that the next actor will read deeper from the
505    // init-contexts.
506    (
507        init_contexts: $init_contexts:expr,
508        store: $store:expr,
509        done: [$($done:ty,)*],
510        todo: [$current:ty, $($todo:ty,)*],
511        futures: [
512            $($futures:expr,)*
513        ],
514    ) => {
515        $crate::make_futures! {
516            init_contexts: $init_contexts,
517            store: $store,
518            done: [$($done,)* $current,],
519            todo: [$($todo,)*],
520            futures: [
521                $($futures,)*
522                core::pin::pin!(
523                    $crate::__exports::execute_actor::<$current>(
524                        $store,
525                        $crate::__read_cons! {
526                            from: $init_contexts,
527                            depth: [$($done)*],
528                        },
529                    )
530                ),
531            ],
532        }
533    };
534}
535
536#[doc(hidden)]
537#[macro_export]
538macro_rules! discard_to_unit {
539    ($_:tt) => {
540        ()
541    };
542}