Skip to main content

proxima_primitives/pipe/
primitives.rs

1//! The pipe primitives: the no_std + no-alloc root the whole algebra is built from.
2//!
3//! [`Pipe`] is the root form — typed In/Out/Err, RPITIT, NO `Send` bound. This
4//! INVERTS the legacy arrangement: local is root, `Send` is the additive
5//! constraint ([`SendPipe`]), because Send-everywhere is a work-stealing
6//! assumption and prime is per-core shared-nothing. [`AndThen`] is the
7//! composition law (`Second::Err: From<First::Err>` at the call site); the
8//! 13 `proxima-core` markers AND-propagate through it.
9//!
10//! Nothing here allocates or touches `std`: it compiles on a bare
11//! `#![no_std]` target (proven by the crate's core cliff build).
12
13use core::fmt::Debug;
14use core::future::Future;
15use core::pin::Pin;
16use core::task::{Context, Poll};
17
18/// Core-tier root form: typed In/Out/Err, no `Send` bound on `Self` or the
19/// returned future. The minimal contract a pipe must satisfy; usable on a
20/// per-core, shared-nothing `!Send` worker (`Rc`, `RefCell`, per-core arenas).
21///
22/// `Err` is an associated type so error interpretation is the only place the
23/// signature carries protocol meaning; nominal byte pipes set `Err` to their
24/// protocol error.
25///
26/// # The four forms
27///
28/// There are not four traits to learn. Pick `In` and `Out` — where `()` is
29/// Rust's "nothing" value — and this one trait becomes four familiar shapes.
30/// Only `transform` is load-bearing; the other three are it, degenerated:
31///
32/// | form      | shape       | what it is                                |
33/// |-----------|-------------|-------------------------------------------|
34/// | transform | `In -> Out` | turns one thing into another              |
35/// | source    | `() -> Out` | takes nothing, produces something         |
36/// | sink      | `In -> ()`  | takes something, produces nothing         |
37/// | observe   | `In -> In`  | hands back its input; acts on the side    |
38///
39/// All four, as real code — this block is compiled by `cargo test`, so it
40/// cannot describe a trait that no longer exists:
41///
42/// ```
43/// use core::convert::Infallible;
44/// use core::future::Future;
45/// use proxima_primitives::pipe::Pipe;
46///
47/// // transform: In -> Out
48/// struct Double;
49/// impl Pipe for Double {
50///     type In = u64;
51///     type Out = u64;
52///     type Err = Infallible;
53///     fn call(&self, input: u64) -> impl Future<Output = Result<u64, Infallible>> {
54///         async move { Ok(input * 2) }
55///     }
56/// }
57///
58/// // source: () -> Out. Nothing goes in.
59/// struct Always;
60/// impl Pipe for Always {
61///     type In = ();
62///     type Out = u64;
63///     type Err = Infallible;
64///     fn call(&self, _input: ()) -> impl Future<Output = Result<u64, Infallible>> {
65///         async move { Ok(7) }
66///     }
67/// }
68///
69/// // sink: In -> (). Nothing comes out.
70/// struct Discard;
71/// impl Pipe for Discard {
72///     type In = u64;
73///     type Out = ();
74///     type Err = Infallible;
75///     fn call(&self, _input: u64) -> impl Future<Output = Result<(), Infallible>> {
76///         async move { Ok(()) }
77///     }
78/// }
79///
80/// // observe: In -> In. Out == In is what makes it an observe.
81/// struct Echo;
82/// impl Pipe for Echo {
83///     type In = u64;
84///     type Out = u64;
85///     type Err = Infallible;
86///     fn call(&self, input: u64) -> impl Future<Output = Result<u64, Infallible>> {
87///         async move { Ok(input) }
88///     }
89/// }
90/// ```
91pub trait Pipe {
92    /// Input the pipe consumes.
93    type In;
94    /// Output the pipe produces.
95    type Out;
96    /// Error the pipe can fail with. `Debug + 'static`; `Send` is added only on
97    /// the cross-core [`SendPipe`] form.
98    type Err: Debug + 'static;
99
100    /// Apply the pipe. The returned future is NOT required to be `Send`.
101    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>>;
102}
103
104/// Additive cross-core form: the pipe and its returned future are `Send`, so it
105/// can be dispatched across cores. Standalone (not `SendPipe: Pipe`) because an
106/// RPITIT future's `Send`-ness cannot be strengthened by a subtrait on stable.
107///
108/// There is no blanket bridge from [`Pipe`], and there cannot be one: writing
109/// `impl<P: Pipe + Send> SendPipe for P` requires bounding `P::call`'s returned
110/// future — a bound on an RPITIT return type, i.e. return-type notation, which
111/// is unstable (rust#109417). So each additive constraint costs a full standalone
112/// copy of the contract. When RTN stabilises, every tier below collapses back
113/// into `Pipe` plus a bound at the use site, and these traits are deletable.
114pub trait SendPipe: Send + Sync + 'static {
115    /// Input the pipe consumes.
116    type In;
117    /// Output the pipe produces.
118    type Out;
119    /// Error the pipe can fail with — `Send` here so it can cross cores.
120    type Err: Debug + Send + 'static;
121
122    /// Apply the pipe. The returned future IS `Send`.
123    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> + Send;
124}
125
126/// Additive in-place form: the returned future is `Unpin`, so a caller can poll
127/// it where it sits — `Pin::new(&mut fut)` is safe. No `unsafe`, no `Box`, no
128/// allocation, no pin-projection.
129///
130/// `Unpin` is the rung after `Send` on the same ladder: `Pipe` (borrow, `!Send`)
131/// → `+ 'static` (own it, erase) → `+ Send` (cross a core) → `+ Unpin` (poll in
132/// place). Climb only as far as the use demands. What it buys: a caller holding
133/// several of these can ask each "anything ready?" and poll them where they
134/// sit — on a bare target, with no heap.
135///
136/// The cost is real and belongs to the impl: an `async move { .. }` block is
137/// `!Unpin`, so an implementor returns a hand-written poll struct instead — a
138/// `Future` with a `poll`, which is what a ring-backed source already is.
139///
140/// Standalone for the same reason [`SendPipe`] is: no blanket bridge is
141/// expressible on stable (see `SendPipe`'s note on rust#109417).
142pub trait UnpinPipe {
143    /// Input the pipe consumes.
144    type In;
145    /// Output the pipe produces.
146    type Out;
147    /// Error the pipe can fail with.
148    type Err: Debug + 'static;
149
150    /// Apply the pipe. The returned future IS `Unpin` — pollable in place.
151    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> + Unpin;
152}
153
154/// Both additive constraints at once: the pipe and its future are `Send`, and
155/// the future is `Unpin` — the top rung, crossing a core AND pollable in place.
156///
157/// Two constraints means four standalone traits, because none of them compose on
158/// stable: `Pipe`, `+ Send`, `+ Unpin`, `+ Send + Unpin`. That tax is the direct
159/// cost of rust#109417; with return-type notation every rung collapses back into
160/// `Pipe` plus a bound at the use site, and all three of these are deletable.
161///
162/// Do not reach for this rung by default. Wanting it usually means a caller is
163/// paying to poll `Send` futures in place; wanting it *speculatively* means
164/// nothing needs it yet.
165pub trait UnpinSendPipe: Send + Sync + 'static {
166    /// Input the pipe consumes.
167    type In;
168    /// Output the pipe produces.
169    type Out;
170    /// Error the pipe can fail with — `Send` here so it can cross cores.
171    type Err: Debug + Send + 'static;
172
173    /// Apply the pipe. The returned future is `Send` AND `Unpin`.
174    fn call(
175        &self,
176        input: Self::In,
177    ) -> impl Future<Output = Result<Self::Out, Self::Err>> + Send + Unpin;
178}
179
180/// Type-chained sequence: feed `input` through `first`, then feed the
181/// intermediate through `second`. The type system enforces
182/// `first::Out = second::In`; the error channel is bridged by
183/// `Second::Err: From<First::Err>` (thiserror `#[from]` chains supply the
184/// impls). This is the composition law of the form family — the only operator
185/// the backport lands at G2; Race/Tee/Quorum follow when a use case asks.
186///
187/// `AndThen` is itself a [`Pipe`] (and a [`SendPipe`] when both stages are), so
188/// chains nest: `AndThen::new(a, AndThen::new(b, c))`.
189#[derive(Debug, Clone, Copy)]
190pub struct AndThen<First, Second> {
191    first: First,
192    second: Second,
193}
194
195impl<First, Second> AndThen<First, Second> {
196    /// Construct a two-stage `AndThen`. Nest for longer chains.
197    #[must_use]
198    pub const fn new(first: First, second: Second) -> Self {
199        Self { first, second }
200    }
201}
202
203impl<First, Second> Pipe for AndThen<First, Second>
204where
205    First: Pipe,
206    Second: Pipe<In = First::Out>,
207    Second::Err: From<First::Err>,
208{
209    type In = First::In;
210    type Out = Second::Out;
211    type Err = Second::Err;
212
213    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> {
214        async move {
215            // `?` converts First::Err into Second::Err via the From bound at
216            // this composition site — the error law lives here, not on the trait.
217            let intermediate = self.first.call(input).await?;
218            self.second.call(intermediate).await
219        }
220    }
221}
222
223impl<First, Second> SendPipe for AndThen<First, Second>
224where
225    First: SendPipe,
226    Second: SendPipe<In = First::Out>,
227    Second::Err: From<First::Err>,
228    First::In: Send,
229{
230    type In = First::In;
231    type Out = Second::Out;
232    type Err = Second::Err;
233
234    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> + Send {
235        async move {
236            let intermediate = self.first.call(input).await?;
237            self.second.call(intermediate).await
238        }
239    }
240}
241
242/// Hand-written poll state machine backing `AndThen`'s [`UnpinPipe`]/
243/// [`UnpinSendPipe`] impls — no `Box::pin`, no `async move` (a compiler-
244/// generated async block is not provably `Unpin`, which is the entire reason
245/// this tier needs a hand-written `Future` in the first place). Two states,
246/// `First` then `Second`, matching the two pipe stages.
247///
248/// The second stage's future type is RPITIT — unnameable from outside its own
249/// impl — so it cannot be a field of `First` the way `future: FirstFut` is.
250/// `next` carries it instead, as a closure produced but not yet CALLED
251/// (`Option` so `poll` can move it out once, on the `First -> Second`
252/// transition). This is exactly [`start_and_then`]'s reason for existing: see
253/// its doc for how `SecondFut` gets resolved with no named type anywhere.
254enum AndThenUnpinCall<FirstFut, Next, SecondFut> {
255    First {
256        future: FirstFut,
257        next: Option<Next>,
258    },
259    Second {
260        future: SecondFut,
261    },
262}
263
264/// Construct [`AndThenUnpinCall`]'s `First` state. A free fn, not a bare
265/// struct literal, because `SecondFut` appears in NEITHER argument here —
266/// only in the `next: FnOnce(Intermediate) -> SecondFut` bound below — and a
267/// struct literal gives the compiler nothing to solve a missing field's type
268/// from. A function-level generic parameter does: `next`'s CONCRETE closure
269/// type uniquely determines `SecondFut` via its own `FnOnce` impl (the same
270/// mechanism that lets `Option::map`/`Iterator::map` infer their output type
271/// from a closure with no type annotation anywhere).
272fn start_and_then<FirstFut, Intermediate, FirstErr, Next, SecondFut>(
273    future: FirstFut,
274    next: Next,
275) -> AndThenUnpinCall<FirstFut, Next, SecondFut>
276where
277    FirstFut: Future<Output = Result<Intermediate, FirstErr>>,
278    Next: FnOnce(Intermediate) -> SecondFut,
279{
280    AndThenUnpinCall::First {
281        future,
282        next: Some(next),
283    }
284}
285
286impl<FirstFut, Intermediate, FirstErr, Next, SecondFut, SecondOut, SecondErr> Future
287    for AndThenUnpinCall<FirstFut, Next, SecondFut>
288where
289    FirstFut: Future<Output = Result<Intermediate, FirstErr>> + Unpin,
290    Next: FnOnce(Intermediate) -> SecondFut + Unpin,
291    SecondFut: Future<Output = Result<SecondOut, SecondErr>> + Unpin,
292    SecondErr: From<FirstErr>,
293{
294    type Output = Result<SecondOut, SecondErr>;
295
296    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
297        // `Self: Unpin` follows structurally from the bounds above (every
298        // field is `Unpin`), so `get_mut` needs no `unsafe` pin projection.
299        let this = self.get_mut();
300        loop {
301            match this {
302                AndThenUnpinCall::First { future, next } => {
303                    match Pin::new(future).poll(cx) {
304                        Poll::Ready(Ok(intermediate)) => {
305                            // `next` is `None` only if this future is polled
306                            // again after already resolving — not a memory
307                            // safety issue, so parking here (rather than
308                            // panicking) is the house style for that case.
309                            let Some(next) = next.take() else {
310                                return Poll::Pending;
311                            };
312                            *this = AndThenUnpinCall::Second {
313                                future: next(intermediate),
314                            };
315                            // loop: the Second stage may already be ready.
316                        }
317                        Poll::Ready(Err(err)) => return Poll::Ready(Err(SecondErr::from(err))),
318                        Poll::Pending => return Poll::Pending,
319                    }
320                }
321                AndThenUnpinCall::Second { future } => return Pin::new(future).poll(cx),
322            }
323        }
324    }
325}
326
327impl<First, Second> UnpinPipe for AndThen<First, Second>
328where
329    First: UnpinPipe,
330    Second: UnpinPipe<In = First::Out>,
331    Second::Err: From<First::Err>,
332{
333    type In = First::In;
334    type Out = Second::Out;
335    type Err = Second::Err;
336
337    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> + Unpin {
338        let second = &self.second;
339        start_and_then(self.first.call(input), move |intermediate| {
340            second.call(intermediate)
341        })
342    }
343}
344
345impl<First, Second> UnpinSendPipe for AndThen<First, Second>
346where
347    First: UnpinSendPipe,
348    Second: UnpinSendPipe<In = First::Out>,
349    Second::Err: From<First::Err>,
350{
351    type In = First::In;
352    type Out = Second::Out;
353    type Err = Second::Err;
354
355    fn call(
356        &self,
357        input: Self::In,
358    ) -> impl Future<Output = Result<Self::Out, Self::Err>> + Send + Unpin {
359        let second = &self.second;
360        start_and_then(self.first.call(input), move |intermediate| {
361            second.call(intermediate)
362        })
363    }
364}
365
366// Marker propagation — an `AndThen` carries a marker only when BOTH stages do.
367// AND-composition via blanket impls (mirrors the legacy proxima-process
368// operators.rs and proxima-core's marker doc: OR-propagation would need
369// overlapping blankets / specialization). The markers live in
370// `proxima-core::markers`; `AndThen` is local, so these foreign-trait /
371// local-type impls are coherence-free.
372mod marker_propagation {
373    use super::AndThen;
374    use proxima_core::markers::{
375        AllocFree, Commutative, Deterministic, DropSafe, IdempotentSideEffectFree, IsPure, NoStd,
376        Reproducible, WithoutFilesystem, WithoutNetwork, WithoutRandom, WithoutSpawn, WithoutTime,
377    };
378
379    impl<First: DropSafe, Second: DropSafe> DropSafe for AndThen<First, Second> {}
380    impl<First: NoStd, Second: NoStd> NoStd for AndThen<First, Second> {}
381    impl<First: AllocFree, Second: AllocFree> AllocFree for AndThen<First, Second> {}
382    impl<First: IsPure, Second: IsPure> IsPure for AndThen<First, Second> {}
383    impl<First: Deterministic, Second: Deterministic> Deterministic for AndThen<First, Second> {}
384    impl<First: Reproducible, Second: Reproducible> Reproducible for AndThen<First, Second> {}
385    impl<First: IdempotentSideEffectFree, Second: IdempotentSideEffectFree> IdempotentSideEffectFree
386        for AndThen<First, Second>
387    {
388    }
389    impl<First: Commutative, Second: Commutative> Commutative for AndThen<First, Second> {}
390
391    impl<First: WithoutFilesystem, Second: WithoutFilesystem> WithoutFilesystem
392        for AndThen<First, Second>
393    {
394    }
395    impl<First: WithoutNetwork, Second: WithoutNetwork> WithoutNetwork for AndThen<First, Second> {}
396    impl<First: WithoutSpawn, Second: WithoutSpawn> WithoutSpawn for AndThen<First, Second> {}
397    impl<First: WithoutTime, Second: WithoutTime> WithoutTime for AndThen<First, Second> {}
398    impl<First: WithoutRandom, Second: WithoutRandom> WithoutRandom for AndThen<First, Second> {}
399}
400
401#[cfg(test)]
402#[allow(clippy::unwrap_used, clippy::expect_used)]
403mod tests {
404    use super::{AndThen, Pipe, SendPipe};
405    use crate::block_on;
406    use crate::pipe::ext::PipeExt;
407    use core::convert::Infallible;
408    use core::future::Future;
409    use core::marker::PhantomData;
410    use proxima_core::markers::{
411        AllocFree, Commutative, Deterministic, DropSafe, IdempotentSideEffectFree, IsPure, NoStd,
412        Reproducible, WithoutFilesystem, WithoutNetwork, WithoutRandom, WithoutSpawn, WithoutTime,
413    };
414
415    /// Pure passthrough probe: carries every marker, impls both forms so it
416    /// exercises `AndThen` on the no-Send root AND the cross-core form.
417    /// `PhantomData<fn() -> Value>` keeps it `Send + Sync` for any `Value`.
418    struct Identity<Value>(PhantomData<fn() -> Value>);
419
420    impl<Value> Identity<Value> {
421        const fn new() -> Self {
422            Self(PhantomData)
423        }
424    }
425
426    impl<Value> Pipe for Identity<Value> {
427        type In = Value;
428        type Out = Value;
429        type Err = Infallible;
430
431        fn call(&self, input: Value) -> impl Future<Output = Result<Value, Infallible>> {
432            async move { Ok(input) }
433        }
434    }
435
436    impl<Value: Send + 'static> SendPipe for Identity<Value> {
437        type In = Value;
438        type Out = Value;
439        type Err = Infallible;
440
441        fn call(&self, input: Value) -> impl Future<Output = Result<Value, Infallible>> + Send {
442            async move { Ok(input) }
443        }
444    }
445
446    impl<Value> DropSafe for Identity<Value> {}
447    impl<Value> NoStd for Identity<Value> {}
448    impl<Value> AllocFree for Identity<Value> {}
449    impl<Value> IsPure for Identity<Value> {}
450    impl<Value> Deterministic for Identity<Value> {}
451    impl<Value> Reproducible for Identity<Value> {}
452    impl<Value> IdempotentSideEffectFree for Identity<Value> {}
453    impl<Value> Commutative for Identity<Value> {}
454    impl<Value> WithoutFilesystem for Identity<Value> {}
455    impl<Value> WithoutNetwork for Identity<Value> {}
456    impl<Value> WithoutSpawn for Identity<Value> {}
457    impl<Value> WithoutTime for Identity<Value> {}
458    impl<Value> WithoutRandom for Identity<Value> {}
459
460    /// Fallible probe with its own error type — proves a non-trivial `Err`.
461    #[derive(Debug, PartialEq, Eq)]
462    struct Overflow;
463
464    struct Increment;
465
466    impl Pipe for Increment {
467        type In = u64;
468        type Out = u64;
469        type Err = Overflow;
470
471        fn call(&self, input: u64) -> impl Future<Output = Result<u64, Overflow>> {
472            async move { input.checked_add(1).ok_or(Overflow) }
473        }
474    }
475
476    /// Second-stage probe whose error type is DISTINCT from `Increment`'s but
477    /// absorbs it via `From` — this is what makes `AndThen<Increment, Halve>`
478    /// type-check, proving the composition law `Second::Err: From<First::Err>`.
479    #[derive(Debug, PartialEq, Eq)]
480    enum HalveError {
481        Odd,
482        Upstream(Overflow),
483    }
484
485    impl From<Overflow> for HalveError {
486        fn from(value: Overflow) -> Self {
487            HalveError::Upstream(value)
488        }
489    }
490
491    struct Halve;
492
493    impl Pipe for Halve {
494        type In = u64;
495        type Out = u64;
496        type Err = HalveError;
497
498        fn call(&self, input: u64) -> impl Future<Output = Result<u64, HalveError>> {
499            async move {
500                if input.is_multiple_of(2) {
501                    Ok(input / 2)
502                } else {
503                    Err(HalveError::Odd)
504                }
505            }
506        }
507    }
508
509    /// Third-stage probe whose error absorbs BOTH upstream error types via
510    /// `From` — proves the two nestings of a 3-stage chain
511    /// (`a.and_then(b).and_then(c)` vs `AndThen::new(a, AndThen::new(b, c))`)
512    /// both type-check and agree on output.
513    #[derive(Debug, PartialEq, Eq)]
514    enum DoubleError {
515        TooBig,
516        Halve(HalveError),
517    }
518
519    impl From<HalveError> for DoubleError {
520        fn from(value: HalveError) -> Self {
521            DoubleError::Halve(value)
522        }
523    }
524
525    impl From<Overflow> for DoubleError {
526        fn from(value: Overflow) -> Self {
527            DoubleError::Halve(HalveError::from(value))
528        }
529    }
530
531    struct Double;
532
533    impl Pipe for Double {
534        type In = u64;
535        type Out = u64;
536        type Err = DoubleError;
537
538        fn call(&self, input: u64) -> impl Future<Output = Result<u64, DoubleError>> {
539            async move { input.checked_mul(2).ok_or(DoubleError::TooBig) }
540        }
541    }
542
543    /// Always-erroring first stage — used to prove a downstream stage never
544    /// runs when an upstream stage short-circuits the chain.
545    struct AlwaysFail;
546
547    impl Pipe for AlwaysFail {
548        type In = u64;
549        type Out = u64;
550        type Err = Overflow;
551
552        fn call(&self, _input: u64) -> impl Future<Output = Result<u64, Overflow>> {
553            async move { Err(Overflow) }
554        }
555    }
556
557    /// Records whether it was ever invoked, via a caller-owned flag borrowed
558    /// for the probe's lifetime — proves `AndThen::call`'s `?` short-circuit
559    /// truly never reaches the second stage, not just that the output matches.
560    struct SpyRef<'flag> {
561        ran: &'flag core::cell::Cell<bool>,
562    }
563
564    impl Pipe for SpyRef<'_> {
565        type In = u64;
566        type Out = u64;
567        type Err = HalveError;
568
569        fn call(&self, input: u64) -> impl Future<Output = Result<u64, HalveError>> {
570            self.ran.set(true);
571            async move { Ok(input) }
572        }
573    }
574
575    #[test]
576    fn series_pipes_identity_through_identity() {
577        // Identity implements BOTH Pipe and SendPipe, so `.and_then()` would be
578        // ambiguous (E0034) between PipeExt and SendPipeExt without a
579        // fully-qualified call, which reads worse than the explicit
580        // constructor. Left as AndThen::new.
581        let chain = AndThen::new(Identity::<u64>::new(), Identity::<u64>::new());
582        let out = block_on(Pipe::call(&chain, 41)).expect("infallible identity chain");
583        assert_eq!(out, 41);
584    }
585
586    #[test]
587    fn series_composes_distinct_error_types_via_from() {
588        // 5 -> increment -> 6 -> halve -> 3. Both stages succeed.
589        let chain = Increment.and_then(Halve);
590        let out = block_on(Pipe::call(&chain, 5)).expect("even intermediate halves");
591        assert_eq!(out, 3);
592    }
593
594    #[test]
595    fn series_propagates_first_stage_error_converted_by_from() {
596        // u64::MAX -> increment overflows -> Overflow -> From -> HalveError::Upstream.
597        let chain = Increment.and_then(Halve);
598        let err = block_on(Pipe::call(&chain, u64::MAX)).expect_err("overflow propagates");
599        assert_eq!(err, HalveError::Upstream(Overflow));
600    }
601
602    #[test]
603    fn series_surfaces_second_stage_error_unchanged() {
604        // 6 -> increment -> 7 (odd) -> halve rejects.
605        let chain = Increment.and_then(Halve);
606        let err = block_on(Pipe::call(&chain, 6)).expect_err("odd intermediate rejected");
607        assert_eq!(err, HalveError::Odd);
608    }
609
610    fn assert_send<Type: Send>(_: &Type) {}
611
612    #[test]
613    fn series_is_a_send_pipe_with_a_send_future() {
614        // same ambiguity as series_pipes_identity_through_identity — Identity
615        // implements both Pipe and SendPipe, so `.and_then()` is ambiguous here.
616        let chain = AndThen::new(Identity::<u64>::new(), Identity::<u64>::new());
617        let future = SendPipe::call(&chain, 7);
618        assert_send(&future);
619        let out = block_on(future).expect("send chain");
620        assert_eq!(out, 7);
621    }
622
623    // compile-time proof: an AndThen of two fully-marked pipes carries every
624    // marker. If propagation regressed, this fails to compile — the marker
625    // gates are structural, not asserted at runtime.
626    fn assert_all_markers<Type>()
627    where
628        Type: NoStd
629            + AllocFree
630            + IsPure
631            + Deterministic
632            + Reproducible
633            + IdempotentSideEffectFree
634            + Commutative
635            + WithoutFilesystem
636            + WithoutNetwork
637            + WithoutSpawn
638            + WithoutTime
639            + WithoutRandom
640            + DropSafe,
641    {
642    }
643
644    #[test]
645    fn series_propagates_all_markers_from_both_stages() {
646        assert_all_markers::<AndThen<Identity<u64>, Identity<u64>>>();
647        // nested chains still carry the markers (AND of AND).
648        assert_all_markers::<AndThen<Identity<u64>, AndThen<Identity<u64>, Identity<u64>>>>();
649    }
650
651    #[test]
652    fn and_then_matches_series_new_for_a_two_stage_chain() {
653        let fluent = block_on(Pipe::call(&Increment.and_then(Halve), 5));
654        let manual = block_on(Pipe::call(&AndThen::new(Increment, Halve), 5));
655        assert_eq!(fluent, manual);
656        assert_eq!(fluent.expect("even intermediate halves"), 3);
657    }
658
659    #[test]
660    fn and_then_matches_series_new_on_the_error_path() {
661        let fluent = block_on(Pipe::call(&Increment.and_then(Halve), u64::MAX));
662        let manual = block_on(Pipe::call(&AndThen::new(Increment, Halve), u64::MAX));
663        assert_eq!(fluent, manual);
664        assert_eq!(
665            fluent.expect_err("overflow propagates"),
666            HalveError::Upstream(Overflow)
667        );
668    }
669
670    #[test]
671    fn and_then_chain_matches_nested_series_new_for_a_three_stage_chain() {
672        // a.and_then(b).and_then(c) associates as AndThen::new(AndThen::new(a, b), c);
673        // AndThen::new(a, AndThen::new(b, c)) associates the other way. Both are
674        // legal compositions of the same three stages and must agree on output.
675        let left_leaning = Increment.and_then(Halve).and_then(Double);
676        let right_leaning = AndThen::new(Increment, AndThen::new(Halve, Double));
677
678        let left_output = block_on(Pipe::call(&left_leaning, 5)).expect("5 -> 6 -> 3 -> 6");
679        let right_output = block_on(Pipe::call(&right_leaning, 5)).expect("5 -> 6 -> 3 -> 6");
680        assert_eq!(left_output, right_output);
681        assert_eq!(left_output, 6);
682    }
683
684    #[test]
685    fn and_then_short_circuits_before_the_second_stage_on_first_stage_error() {
686        let ran = core::cell::Cell::new(false);
687        let chain = AlwaysFail.and_then(SpyRef { ran: &ran });
688
689        let err = block_on(Pipe::call(&chain, 1)).expect_err("first stage always fails");
690
691        assert_eq!(err, HalveError::Upstream(Overflow));
692        assert!(
693            !ran.get(),
694            "second stage must not run after first stage errors"
695        );
696    }
697}
698
699#[cfg(test)]
700#[allow(clippy::unwrap_used, clippy::expect_used)]
701mod unpin_tier_tests {
702    use super::{AndThen, UnpinPipe, UnpinSendPipe};
703    use core::convert::Infallible;
704    use core::future::Future;
705    use core::pin::Pin;
706    use core::task::{Context, Poll, Waker};
707
708    // What an `Unpin` source actually is: a hand-written poll struct, not an
709    // async block. A ring pop is exactly this shape already.
710    struct RingPop(u8);
711    impl Future for RingPop {
712        type Output = Result<u8, Infallible>;
713        fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
714            Poll::Ready(Ok(self.0))
715        }
716    }
717
718    struct Ring(u8);
719    impl UnpinPipe for Ring {
720        type In = ();
721        type Out = u8;
722        type Err = Infallible;
723        fn call(&self, (): ()) -> impl Future<Output = Result<u8, Infallible>> + Unpin {
724            RingPop(self.0)
725        }
726    }
727    impl UnpinSendPipe for Ring {
728        type In = ();
729        type Out = u8;
730        type Err = Infallible;
731        fn call(&self, (): ()) -> impl Future<Output = Result<u8, Infallible>> + Send + Unpin {
732            RingPop(self.0)
733        }
734    }
735
736    // THE POINT: poll N sources in place. No unsafe, no Box, no alloc, and no
737    // future is abandoned — each is polled where it sits.
738    fn merge_in_place<S: UnpinPipe<In = ()>, const N: usize>(
739        sources: &[S; N],
740        cx: &mut Context<'_>,
741    ) -> Poll<Result<S::Out, S::Err>> {
742        for source in sources {
743            let mut call = source.call(());
744            if let Poll::Ready(out) = Pin::new(&mut call).poll(cx) {
745                return Poll::Ready(out);
746            }
747        }
748        Poll::Pending
749    }
750
751    #[test]
752    fn unpin_future_polls_in_place_without_unsafe_or_alloc() {
753        let waker = Waker::noop();
754        let mut cx = Context::from_waker(waker);
755        let sources = [Ring(7), Ring(9)];
756        match merge_in_place(&sources, &mut cx) {
757            Poll::Ready(Ok(value)) => assert_eq!(value, 7, "first ready source wins"),
758            other => panic!("expected the first source to be ready, got {other:?}"),
759        }
760    }
761
762    #[test]
763    fn send_unpin_tier_is_the_cross_core_mergeable_form() {
764        fn assert_send_unpin<S: UnpinSendPipe<In = ()>>(source: &S) {
765            fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
766            let call = UnpinSendPipe::call(source, ());
767            needs_send_unpin(&call);
768        }
769        assert_send_unpin(&Ring(1));
770    }
771
772    // second-stage probe: doubles, always immediately ready — an Unpin pipe
773    // that never suspends, the ring-pop shape `core::future::ready` gives for
774    // free.
775    struct Doubler;
776    impl UnpinPipe for Doubler {
777        type In = u8;
778        type Out = u16;
779        type Err = Infallible;
780        fn call(&self, input: u8) -> impl Future<Output = Result<u16, Infallible>> + Unpin {
781            core::future::ready(Ok(u16::from(input) * 2))
782        }
783    }
784    impl UnpinSendPipe for Doubler {
785        type In = u8;
786        type Out = u16;
787        type Err = Infallible;
788        fn call(&self, input: u8) -> impl Future<Output = Result<u16, Infallible>> + Send + Unpin {
789            core::future::ready(Ok(u16::from(input) * 2))
790        }
791    }
792
793    #[test]
794    fn and_then_unpin_chain_composes_two_pipes_without_a_box() {
795        let waker = Waker::noop();
796        let mut cx = Context::from_waker(waker);
797        let chain = AndThen::new(Ring(21), Doubler);
798        let mut call = UnpinPipe::call(&chain, ());
799        match Pin::new(&mut call).poll(&mut cx) {
800            Poll::Ready(Ok(value)) => assert_eq!(value, 42, "21 -> ring pop -> double -> 42"),
801            other => panic!("expected ready, got {other:?}"),
802        }
803    }
804
805    #[test]
806    fn and_then_unpin_send_chain_is_send_and_unpin() {
807        fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
808        let chain = AndThen::new(Ring(5), Doubler);
809        let call = UnpinSendPipe::call(&chain, ());
810        needs_send_unpin(&call);
811    }
812
813    // first-stage probe: reports `Pending` exactly once (registering the
814    // waker), `Ready` on every poll after — proves the AndThen state machine
815    // genuinely resumes across separate `poll()` calls (not just when both
816    // stages resolve on the first poll) without re-running the first stage.
817    struct PendOnceThenReady {
818        value: u8,
819        polled_once: core::cell::Cell<bool>,
820    }
821    impl Future for PendOnceThenReady {
822        type Output = Result<u8, Infallible>;
823        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
824            if self.polled_once.replace(true) {
825                Poll::Ready(Ok(self.value))
826            } else {
827                cx.waker().wake_by_ref();
828                Poll::Pending
829            }
830        }
831    }
832
833    struct SlowRing(u8);
834    impl UnpinPipe for SlowRing {
835        type In = ();
836        type Out = u8;
837        type Err = Infallible;
838        fn call(&self, (): ()) -> impl Future<Output = Result<u8, Infallible>> + Unpin {
839            PendOnceThenReady {
840                value: self.0,
841                polled_once: core::cell::Cell::new(false),
842            }
843        }
844    }
845
846    #[test]
847    fn and_then_unpin_chain_resumes_across_polls_without_rerunning_first_stage() {
848        let waker = Waker::noop();
849        let mut cx = Context::from_waker(waker);
850        let chain = AndThen::new(SlowRing(21), Doubler);
851        let mut call = UnpinPipe::call(&chain, ());
852        assert_eq!(
853            Pin::new(&mut call).poll(&mut cx),
854            Poll::Pending,
855            "first stage not ready yet"
856        );
857        match Pin::new(&mut call).poll(&mut cx) {
858            Poll::Ready(Ok(value)) => {
859                assert_eq!(value, 42, "second poll resumes at First, then runs Second");
860            }
861            other => panic!("expected ready on the second poll, got {other:?}"),
862        }
863    }
864
865    // first-stage probe that always fails — proves the Unpin chain
866    // short-circuits before the second stage exactly like the async-block
867    // `Pipe`/`SendPipe` forms do.
868    #[derive(Debug, PartialEq, Eq)]
869    struct RingError;
870
871    struct FailingRing;
872    impl UnpinPipe for FailingRing {
873        type In = ();
874        type Out = u8;
875        type Err = RingError;
876        fn call(&self, (): ()) -> impl Future<Output = Result<u8, RingError>> + Unpin {
877            core::future::ready(Err(RingError))
878        }
879    }
880
881    struct CountingDoubler {
882        calls: core::cell::Cell<u32>,
883    }
884    impl UnpinPipe for CountingDoubler {
885        type In = u8;
886        type Out = u16;
887        type Err = RingError;
888        fn call(&self, input: u8) -> impl Future<Output = Result<u16, RingError>> + Unpin {
889            self.calls.set(self.calls.get() + 1);
890            core::future::ready(Ok(u16::from(input) * 2))
891        }
892    }
893
894    #[test]
895    fn and_then_unpin_chain_short_circuits_before_the_second_stage() {
896        let waker = Waker::noop();
897        let mut cx = Context::from_waker(waker);
898        let second = CountingDoubler {
899            calls: core::cell::Cell::new(0),
900        };
901        let chain = AndThen::new(FailingRing, second);
902        let mut call = UnpinPipe::call(&chain, ());
903        match Pin::new(&mut call).poll(&mut cx) {
904            Poll::Ready(Err(RingError)) => {}
905            other => panic!("expected the first stage's error, got {other:?}"),
906        }
907        assert_eq!(
908            chain.second.calls.get(),
909            0,
910            "second stage must not run after first stage errors"
911        );
912    }
913}