Skip to main content

proxima_primitives/pipe/
alloc_tier.rs

1//! Alloc tier: the erased form. Heterogeneous pipes lose their static type and
2//! sit behind a trait object so routing tables, swap cells, and registries can
3//! hold them side by side.
4//!
5//! This is the boundary where `ProximaError` lives: a typed subgraph converts
6//! its `Err` into `ProximaError` exactly here, via `ProximaError: From<P::Err>`
7//! at the erasure blanket. The typed tiers above never name the god-error.
8//!
9//! Two erased forms mirror the two root forms:
10//! - [`DynPipe`] / [`LocalPipeHandle`] erase the no-Send [`Pipe`] (`Rc`).
11//! - [`SendDynPipe`] / [`PipeHandle`] erase the cross-core [`SendPipe`] (`Arc`).
12//!
13//! Needs `alloc` (boxed futures + `Rc`/`Arc`), nothing from `std`.
14
15use alloc::boxed::Box;
16use alloc::rc::Rc;
17use alloc::sync::Arc;
18use core::future::Future;
19use core::pin::Pin;
20
21use proxima_core::ProximaError;
22
23use crate::pipe::primitives::{Pipe, SendPipe};
24
25/// Boxed `!Send` future — the erased return of [`DynPipe::call_dyn`].
26pub type BoxFuture<'a, Output> = Pin<Box<dyn Future<Output = Output> + 'a>>;
27
28/// Boxed `Send` future — the erased return of [`SendDynPipe::call_dyn`].
29pub type SendBoxFuture<'a, Output> = Pin<Box<dyn Future<Output = Output> + Send + 'a>>;
30
31/// Object-safe erasure of the no-Send root [`Pipe`]. The associated future and
32/// `Err` are gone: the future is boxed, the error is pinned to [`ProximaError`].
33/// This is what a `dyn` handle holds.
34pub trait DynPipe<In, Out> {
35    /// Erased dispatch — boxes the inner pipe's future and converts its error
36    /// into [`ProximaError`].
37    fn call_dyn(&self, input: In) -> BoxFuture<'_, Result<Out, ProximaError>>;
38}
39
40impl<P> DynPipe<P::In, P::Out> for P
41where
42    P: Pipe,
43    ProximaError: From<P::Err>,
44{
45    fn call_dyn(&self, input: P::In) -> BoxFuture<'_, Result<P::Out, ProximaError>> {
46        Box::pin(async move { self.call(input).await.map_err(ProximaError::from) })
47    }
48}
49
50/// Erased, shareable no-Send pipe — `Rc` because the root form is per-core.
51pub type LocalPipeHandle<In, Out> = Rc<dyn DynPipe<In, Out>>;
52
53/// Erase a no-Send [`Pipe`] into a shareable [`LocalPipeHandle`].
54pub fn into_local_handle<P>(pipe: P) -> LocalPipeHandle<P::In, P::Out>
55where
56    P: Pipe + 'static,
57    ProximaError: From<P::Err>,
58{
59    Rc::new(pipe)
60}
61
62/// Object-safe erasure of the cross-core [`SendPipe`]. The future is boxed and
63/// `Send`; the error is pinned to [`ProximaError`]. `Send + Sync` so the handle
64/// crosses cores.
65pub trait SendDynPipe<In, Out>: Send + Sync {
66    /// Erased dispatch — boxes the inner pipe's `Send` future and converts its
67    /// error into [`ProximaError`].
68    fn call_dyn(&self, input: In) -> SendBoxFuture<'_, Result<Out, ProximaError>>;
69}
70
71impl<P> SendDynPipe<P::In, P::Out> for P
72where
73    P: SendPipe,
74    P::In: Send,
75    ProximaError: From<P::Err>,
76{
77    fn call_dyn(&self, input: P::In) -> SendBoxFuture<'_, Result<P::Out, ProximaError>> {
78        Box::pin(async move { self.call(input).await.map_err(ProximaError::from) })
79    }
80}
81
82/// Erased, shareable cross-core pipe — `Arc` so it dispatches across cores.
83pub type PipeHandle<In, Out> = Arc<dyn SendDynPipe<In, Out>>;
84
85/// Erase a cross-core [`SendPipe`] into a shareable [`PipeHandle`].
86pub fn into_handle<P>(pipe: P) -> PipeHandle<P::In, P::Out>
87where
88    P: SendPipe,
89    P::In: Send,
90    ProximaError: From<P::Err>,
91{
92    Arc::new(pipe)
93}
94
95// Reflexive re-entry: an erased handle IS itself a pipe of the same form, so
96// it composes wherever a `SendPipe`/`Pipe` is expected (e.g. as an `AndThen`
97// stage, or fed straight back through the `Arc`/`Rc` forwarding impls below).
98// Lives in the leaf (it owns both the erasure traits and the root forms) so
99// this bridge is written ONCE instead of once per domain crate's own
100// hand-rolled `impl SendPipe for dyn XyzDynPipe` copy.
101impl<In: 'static, Out: 'static> SendPipe for dyn SendDynPipe<In, Out> {
102    type In = In;
103    type Out = Out;
104    type Err = ProximaError;
105
106    fn call(&self, input: In) -> impl Future<Output = Result<Out, ProximaError>> + Send {
107        self.call_dyn(input)
108    }
109}
110
111// The base-tier mirror of the impl above: every `SendPipe` is also required
112// to be usable as a plain `Pipe` (the additive tiers never replace the root
113// form), so `PipeHandle` (`Arc<dyn SendDynPipe<..>>`) reaches `PipeExt`'s
114// `.and_then`/`.filter` sugar the same as any other pipe.
115impl<In: 'static, Out: 'static> Pipe for dyn SendDynPipe<In, Out> {
116    type In = In;
117    type Out = Out;
118    type Err = ProximaError;
119
120    fn call(&self, input: In) -> impl Future<Output = Result<Out, ProximaError>> {
121        self.call_dyn(input)
122    }
123}
124
125impl<In, Out> Pipe for dyn DynPipe<In, Out> {
126    type In = In;
127    type Out = Out;
128    type Err = ProximaError;
129
130    fn call(&self, input: In) -> impl Future<Output = Result<Out, ProximaError>> {
131        self.call_dyn(input)
132    }
133}
134
135// Shared-ownership forwarding: an `Arc`/`Rc` of a pipe IS a pipe. Lives in the
136// leaf (it owns the traits) so the cutover's `Arc<P>: Pipe` headers resolve here
137// once, not at every consumer. `Arc` carries the cross-core [`SendPipe`]; `Rc`
138// the per-core [`Pipe`].
139impl<Inner> SendPipe for Arc<Inner>
140where
141    Inner: SendPipe + ?Sized,
142{
143    type In = Inner::In;
144    type Out = Inner::Out;
145    type Err = Inner::Err;
146
147    fn call(
148        &self,
149        input: Self::In,
150    ) -> impl core::future::Future<Output = Result<Self::Out, Self::Err>> + Send {
151        Inner::call(self, input)
152    }
153}
154
155impl<Inner> Pipe for Arc<Inner>
156where
157    Inner: Pipe + ?Sized,
158{
159    type In = Inner::In;
160    type Out = Inner::Out;
161    type Err = Inner::Err;
162
163    fn call(
164        &self,
165        input: Self::In,
166    ) -> impl core::future::Future<Output = Result<Self::Out, Self::Err>> {
167        Inner::call(self, input)
168    }
169}
170
171impl<Inner> Pipe for Rc<Inner>
172where
173    Inner: Pipe + ?Sized,
174{
175    type In = Inner::In;
176    type Out = Inner::Out;
177    type Err = Inner::Err;
178
179    fn call(
180        &self,
181        input: Self::In,
182    ) -> impl core::future::Future<Output = Result<Self::Out, Self::Err>> {
183        Inner::call(self, input)
184    }
185}
186
187#[cfg(test)]
188#[allow(clippy::unwrap_used, clippy::expect_used)]
189mod tests {
190    use super::{SendDynPipe, into_handle, into_local_handle};
191    use crate::block_on;
192    use crate::pipe::primitives::{Pipe, SendPipe};
193    use core::future::Future;
194
195    // Err pinned to ProximaError at the erasure boundary, so the probe uses it
196    // directly (reflexive From<ProximaError>); typed-error pipes supply their
197    // own `impl From<E> for ProximaError`.
198    struct Tagger(u64);
199
200    impl Pipe for Tagger {
201        type In = u64;
202        type Out = u64;
203        type Err = proxima_core::ProximaError;
204
205        fn call(
206            &self,
207            input: u64,
208        ) -> impl Future<Output = Result<u64, proxima_core::ProximaError>> {
209            let tag = self.0;
210            async move { Ok(input + tag) }
211        }
212    }
213
214    impl SendPipe for Tagger {
215        type In = u64;
216        type Out = u64;
217        type Err = proxima_core::ProximaError;
218
219        fn call(
220            &self,
221            input: u64,
222        ) -> impl Future<Output = Result<u64, proxima_core::ProximaError>> + Send {
223            let tag = self.0;
224            async move { Ok(input + tag) }
225        }
226    }
227
228    #[test]
229    fn local_handle_erases_a_no_send_pipe() {
230        let handle = into_local_handle(Tagger(10));
231        let out = block_on(handle.call_dyn(5)).expect("erased local dispatch");
232        assert_eq!(out, 15);
233    }
234
235    #[test]
236    fn send_handle_erases_a_cross_core_pipe() {
237        let handle = into_handle(Tagger(100));
238        let out =
239            block_on(SendDynPipe::call_dyn(handle.as_ref(), 5)).expect("erased send dispatch");
240        assert_eq!(out, 105);
241    }
242
243    fn assert_send_sync<Type: Send + Sync>(_: &Type) {}
244
245    #[test]
246    fn send_handle_is_send_and_sync() {
247        let handle = into_handle(Tagger(1));
248        assert_send_sync(&handle);
249    }
250}